From f56f245c1f7e768caf356a0e7b57f428cf8cc6da Mon Sep 17 00:00:00 2001 From: Tarun Gupta Date: Sun, 26 Jul 2015 13:17:40 +0530 Subject: Update Client. Updates all bits of Client to be in parity with Swiften. Some part of code which depends on Network is commented. License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details. Test-Information: Tests added for: ClientBlockListManager, ClientSession and NickResolver. All tests pass. Change-Id: I5b9b0e5ae5b2df58202e2349ba24e4544d9a4614 diff --git a/src/com/isode/stroke/client/BlockList.java b/src/com/isode/stroke/client/BlockList.java new file mode 100644 index 0000000..bf72f45 --- /dev/null +++ b/src/com/isode/stroke/client/BlockList.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2011-2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.signals.Signal; +import com.isode.stroke.jid.JID; +import java.util.Vector; + +public abstract class BlockList { + + public enum State { + Init, + Requesting, + Available, + Error + }; + + public final Signal onStateChanged = new Signal(); + public final Signal1 onItemAdded = new Signal1(); + public final Signal1 onItemRemoved = new Signal1(); + + public abstract State getState(); + + public abstract Vector getItems(); + + public boolean isBlocked(final JID jid) { + final Vector items = getItems(); + return (items.contains(jid.toBare()) || items.contains(new JID(jid.getDomain())) || items.contains(jid)); + } +} \ No newline at end of file diff --git a/src/com/isode/stroke/client/BlockListImpl.java b/src/com/isode/stroke/client/BlockListImpl.java new file mode 100644 index 0000000..a85f86c --- /dev/null +++ b/src/com/isode/stroke/client/BlockListImpl.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2011 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import com.isode.stroke.client.BlockList; +import com.isode.stroke.jid.JID; +import java.util.Vector; + +public class BlockListImpl extends BlockList { + + private State state; + private Vector items = new Vector(); + + public BlockListImpl() { + this.state = State.Init; + } + + public State getState() { + return state; + } + + public void setState(State state) { + if (!(this.state.equals(state))) { + this.state = state; + onStateChanged.emit(); + } + } + + public Vector getItems() { + return items; + } + + public void setItems(final Vector items) { + for (final JID jid : this.items) { + if(items.contains(jid)) { + onItemRemoved.emit(jid); + } + } + + for (final JID jid : items) { + if(this.items.contains(jid)) { + onItemAdded.emit(jid); + } + } + this.items = items; + } + + public void addItem(final JID item) { + if(!(items.contains(item))) { + items.add(item); + onItemAdded.emit(item); + } + } + + public void removeItem(final JID item) { + int oldSize = items.size(); + while(items.contains(item)) { + items.remove(item); + } + if (items.size() != oldSize) { + onItemRemoved.emit(item); + } + } + + public void addItems(final Vector items) { + Vector itemsToAdd = new Vector(items); //Have to do this to avoid ConcurrentModificationException. + for (final JID item : itemsToAdd) { + addItem(item); + } + } + + public void removeItems(final Vector items) { + Vector itemsToRemove = new Vector(items); //Have to do this to avoid ConcurrentModificationException. + for (final JID item : itemsToRemove) { + removeItem(item); + } + } + + public void removeAllItems() { + removeItems(items); + } +} \ No newline at end of file diff --git a/src/com/isode/stroke/client/Client.java b/src/com/isode/stroke/client/Client.java index 1d137c5..464e062 100644 --- a/src/com/isode/stroke/client/Client.java +++ b/src/com/isode/stroke/client/Client.java @@ -29,6 +29,7 @@ import com.isode.stroke.roster.XMPPRosterImpl; import com.isode.stroke.signals.Signal1; import com.isode.stroke.vcards.VCardManager; import com.isode.stroke.base.SafeByteArray; +import com.isode.stroke.tls.BlindCertificateTrustChecker; /** * Provides the core functionality for writing XMPP client software. @@ -44,7 +45,7 @@ public class Client extends CoreClient { private final DirectedPresenceSender directedPresenceSender; //NOPMD, this is not better as a local variable private final StanzaChannelPresenceSender stanzaChannelPresenceSender; //NOPMD, this is not better as a local variable private final SoftwareVersionResponder softwareVersionResponder; - private final PubSubManager pubSubManager; + private final PubSubManager pubsubManager; private final XMPPRosterImpl roster; private final XMPPRosterController rosterController; private final PresenceOracle presenceOracle; @@ -58,8 +59,13 @@ public class Client extends CoreClient { private final SubscriptionManager subscriptionManager; private final ClientDiscoManager discoManager; private final AvatarManager avatarManager; + //private final JingleSessionManager jingleSessionManager; + //private final FileTransferManager fileTransferManager; + private final BlindCertificateTrustChecker blindCertificateTrustChecker; + //private final WhiteboardSessionManager whiteboardSessionManager; + private final ClientBlockListManager blockListManager; - final Signal1 onPresenceChange = new Signal1(); + public final Signal1 onPresenceChange = new Signal1(); /** * Constructor. @@ -109,9 +115,45 @@ public class Client extends CoreClient { nickManager = new NickManagerImpl(jid.toBare(), vcardManager); nickResolver = new NickResolver(jid.toBare(), roster, vcardManager, mucRegistry); - pubSubManager = new PubSubManagerImpl(getStanzaChannel(), getIQRouter()); + blindCertificateTrustChecker = new BlindCertificateTrustChecker(); + + //TO PORT + //jingleSessionManager = new JingleSessionManager(getIQRouter()); + blockListManager = new ClientBlockListManager(getIQRouter()); + + /*whiteboardSessionManager = NULL; + #ifdef SWIFT_EXPERIMENTAL_WB + whiteboardSessionManager = new WhiteboardSessionManager(getIQRouter(), getStanzaChannel(), presenceOracle, getEntityCapsProvider()); + #endif*/ + + pubsubManager = new PubSubManagerImpl(getStanzaChannel(), getIQRouter()); + + /*#ifdef SWIFT_EXPERIMENTAL_FT + fileTransferManager = new FileTransferManagerImpl( + getJID(), + jingleSessionManager, + getIQRouter(), + getEntityCapsProvider(), + presenceOracle, + getNetworkFactories()->getConnectionFactory(), + getNetworkFactories()->getConnectionServerFactory(), + getNetworkFactories()->getTimerFactory(), + getNetworkFactories()->getDomainNameResolver(), + getNetworkFactories()->getNetworkEnvironment(), + getNetworkFactories()->getNATTraverser(), + getNetworkFactories()->getCryptoProvider()); + #else + fileTransferManager = new DummyFileTransferManager(); + #endif*/ } - + + /** + * Constructs a client for the given JID with the given password. + * + * \param storages The interfaces for storing cache information etc. If + * this is NULL, + * all data will be stored in memory (and be lost on shutdown) + */ public Client(final JID jid, final SafeByteArray password, final NetworkFactories networkFactories) { this(jid, password, networkFactories, null); } @@ -137,13 +179,32 @@ public class Client extends CoreClient { * @return PubSub manager, not null */ public PubSubManager getPubSubManager() { - return pubSubManager; + return pubsubManager; } - + + /** + * Returns a representation of the roster. + * + * The roster is initially empty. To populate it, call requestRoster(), + * which will request the roster from the server. When the roster has + * been requested, it will also be kept up to date when it is updated on + * the server side. + * + * This pointer remains the same across the lifetime of Client. All + * changes to the roster (e.g. after the initial roster request, or after + * subsequent roster updates) are notified through the XMPPRoster's + * signals. + * + * \see requestRoster() + */ public XMPPRoster getRoster() { return roster; } - + + public void setSoftwareVersion(final String name, final String version) { + setSoftwareVersion(name, version, ""); + } + /** * Sets the software version of the client. * @@ -153,7 +214,11 @@ public class Client extends CoreClient { softwareVersionResponder.setVersion(name, version, os); } - + /** + * Requests the roster from the server. + * + * \see getRoster() + */ public void requestRoster() { // FIXME: We should set this once when the session is finished, but there // is currently no callback for this @@ -190,7 +255,37 @@ public class Client extends CoreClient { public ClientDiscoManager getDiscoManager() { return discoManager; } - + + public ClientBlockListManager getClientBlockListManager() { + return blockListManager; + } + + /** + * Returns a FileTransferManager for the client. This is only available after the onConnected + * signal has been fired. + * + * WARNING: File transfer will only work if Swiften is built in 'experimental' mode. + */ + //TO PORT + /*public FileTransferManager getFileTransferManager() { + return fileTransferManager; + }*/ + + /** + * Configures the client to always trust a non-validating + * TLS certificate from the server. + * This is equivalent to setting a BlindCertificateTrustChecker + * using setCertificateTrustChecker(). + */ + public void setAlwaysTrustCertificates() { + setCertificateTrustChecker(blindCertificateTrustChecker); + } + + //TO PORT + /*public WhiteboardSessionManager getWhiteboardSessionManager() { + return whiteboardSessionManager; + }*/ + public VCardManager getVCardManager() { return vcardManager; } @@ -205,7 +300,11 @@ public class Client extends CoreClient { } return memoryStorages; } - + + protected void handleConnected() { + discoManager.handleConnected(); + } + public PresenceSender getPresenceSender() { return discoManager.getPresenceSender(); } diff --git a/src/com/isode/stroke/client/ClientBlockListManager.java b/src/com/isode/stroke/client/ClientBlockListManager.java new file mode 100644 index 0000000..b370f63 --- /dev/null +++ b/src/com/isode/stroke/client/ClientBlockListManager.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2011-2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import com.isode.stroke.elements.BlockPayload; +import com.isode.stroke.elements.BlockListPayload; +import com.isode.stroke.elements.UnblockPayload; +import com.isode.stroke.elements.DiscoInfo; +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.signals.Slot2; +import com.isode.stroke.queries.SetResponder; +import com.isode.stroke.queries.GenericRequest; +import com.isode.stroke.client.BlockList; +import com.isode.stroke.client.BlockListImpl; +import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.jid.JID; +import java.util.Vector; + +public class ClientBlockListManager { + + private IQRouter iqRouter; + private SetResponder blockResponder; + private SetResponder unblockResponder; + private BlockListImpl blockList; + + class BlockResponder extends SetResponder { + + public BlockResponder(BlockListImpl blockList, IQRouter iqRouter) { + super(new BlockPayload(), iqRouter); + this.blockList = blockList; + } + + public boolean handleSetRequest(final JID from, final JID to, final String id, BlockPayload payload) { + if (getIQRouter().isAccountJID(from)) { + if (payload != null) { + blockList.addItems(payload.getItems()); + } + sendResponse(from, id, null); + } + else { + sendError(from, id, ErrorPayload.Condition.NotAuthorized, ErrorPayload.Type.Cancel); + } + return true; + } + + private BlockListImpl blockList; + }; + + class UnblockResponder extends SetResponder { + + public UnblockResponder(BlockListImpl blockList, IQRouter iqRouter) { + super(new UnblockPayload(), iqRouter); + this.blockList = blockList; + } + + public boolean handleSetRequest(final JID from, final JID to, final String id, UnblockPayload payload) { + if (getIQRouter().isAccountJID(from)) { + if (payload != null) { + if (payload.getItems().isEmpty()) { + blockList.removeAllItems(); + } + else { + blockList.removeItems(payload.getItems()); + } + } + sendResponse(from, id, null); + } + else { + sendError(from, id, ErrorPayload.Condition.NotAuthorized, ErrorPayload.Type.Cancel); + } + return true; + } + + private BlockListImpl blockList; + }; + + public ClientBlockListManager(IQRouter iqRouter) { + this.iqRouter = iqRouter; + } + + protected void finalize() throws Throwable { + try { + if (blockList != null && BlockList.State.Available.equals(blockList.getState())) { + unblockResponder.stop(); + blockResponder.stop(); + } + } + finally { + super.finalize(); + } + } + + /** + * Returns the blocklist. + */ + public BlockList getBlockList() { + if (blockList == null) { + blockList = new BlockListImpl(); + blockList.setState(BlockList.State.Init); + } + return blockList; + } + + /** + * Get the blocklist from the server. + */ + public BlockList requestBlockList() { + if (blockList == null) { + blockList = new BlockListImpl(); + } + blockList.setState(BlockList.State.Requesting); + GenericRequest getRequest = new GenericRequest(IQ.Type.Get, new JID(), new BlockListPayload(), iqRouter); + getRequest.onResponse.connect(new Slot2() { + @Override + public void call(BlockListPayload p, ErrorPayload e) { + handleBlockListReceived(p, e); + } + }); + getRequest.send(); + return blockList; + } + + public GenericRequest createBlockJIDRequest(final JID jid) { + Vector vec = new Vector(); + vec.add(jid); + return createBlockJIDsRequest(vec); + } + + public GenericRequest createBlockJIDsRequest(final Vector jids) { + BlockPayload payload = new BlockPayload(jids); + return new GenericRequest(IQ.Type.Set, new JID(), payload, iqRouter); + } + + public GenericRequest createUnblockJIDRequest(final JID jid) { + Vector vec = new Vector(); + vec.add(jid); + return createUnblockJIDsRequest(vec); + } + + public GenericRequest createUnblockJIDsRequest(final Vector jids) { + UnblockPayload payload = new UnblockPayload(jids); + return new GenericRequest(IQ.Type.Set, new JID(), payload, iqRouter); + } + + public GenericRequest createUnblockAllRequest() { + return createUnblockJIDsRequest(new Vector()); + } + + private void handleBlockListReceived(BlockListPayload payload, ErrorPayload error) { + if (error != null || payload == null) { + blockList.setState(BlockList.State.Error); + } + else { + blockList.setItems(payload.getItems()); + blockList.setState(BlockList.State.Available); + if (blockResponder == null) { + blockResponder = new BlockResponder(blockList, iqRouter); + blockResponder.start(); + } + if (unblockResponder == null) { + unblockResponder = new UnblockResponder(blockList, iqRouter); + unblockResponder.start(); + } + } + } +} \ No newline at end of file diff --git a/src/com/isode/stroke/client/ClientError.java b/src/com/isode/stroke/client/ClientError.java index de2db49..cfcf12f 100644 --- a/src/com/isode/stroke/client/ClientError.java +++ b/src/com/isode/stroke/client/ClientError.java @@ -30,9 +30,14 @@ public class ClientError { UnexpectedElementError, ResourceBindError, SessionStartError, + StreamError, TLSError, ClientCertificateLoadError, ClientCertificateError, + + // Certifate on smartcard was removed + CertificateCardRemoved, + // Certificate verification errors UnknownCertificateError, CertificateExpiredError, @@ -45,9 +50,15 @@ public class ClientError { InvalidCertificateSignatureError, InvalidCAError, InvalidServerIdentityError, + RevokedError, + RevocationCheckFailedError }; - ClientError(final Type type) { + public ClientError() { + type_ = Type.UnknownError; + } + + public ClientError(final Type type) { type_ = type; } diff --git a/src/com/isode/stroke/client/ClientOptions.java b/src/com/isode/stroke/client/ClientOptions.java index b410094..c2d9e3f 100644 --- a/src/com/isode/stroke/client/ClientOptions.java +++ b/src/com/isode/stroke/client/ClientOptions.java @@ -10,6 +10,8 @@ package com.isode.stroke.client; import com.isode.stroke.tls.TLSOptions; +import com.isode.stroke.base.URL; +import com.isode.stroke.base.SafeByteArray; /** * Options for a client connection @@ -40,7 +42,17 @@ public class ClientOptions { * Default: false */ public boolean useStreamResumption; - + + /** + * Forget the password once it's used. + * This makes the Client useless after the first login attempt. + * + * FIXME: This is a temporary workaround. + * + * Default: false + */ + public boolean forgetPassword; + /** * Use XEP-0198 acks in the stream when available. * Default: true @@ -48,6 +60,12 @@ public class ClientOptions { public boolean useAcks; /** + * Use Single Sign On. + * Default: false + */ + public boolean singleSignOn; + + /** * The hostname to connect to. * Leave this empty for standard XMPP connection, based on the JID domain. */ @@ -60,6 +78,48 @@ public class ClientOptions { */ public int manualPort; + /** + * The type of proxy to use for connecting to the XMPP + * server. + */ + public ProxyType proxyType; + + /** + * Override the system-configured proxy hostname. + */ + public String manualProxyHostname; + + /** + * Override the system-configured proxy port. + */ + public int manualProxyPort; + + /** + * If non-empty, use BOSH instead of direct TCP, with the given URL. + * Default: empty (no BOSH) + */ + public URL boshURL = new URL(); + + /** + * If non-empty, BOSH connections will try to connect over this HTTP CONNECT + * proxy instead of directly. + * Default: empty (no proxy) + */ + public URL boshHTTPConnectProxyURL = new URL(); + + /** + * If this and matching Password are non-empty, BOSH connections over + * HTTP CONNECT proxies will use these credentials for proxy access. + * Default: empty (no authentication needed by the proxy) + */ + public SafeByteArray boshHTTPConnectProxyAuthID; + public SafeByteArray boshHTTPConnectProxyAuthPassword; + + /** + * This can be initialized with a custom HTTPTrafficFilter, which allows HTTP CONNECT + * proxy initialization to be customized. + */ + //public HTTPTrafficFilter httpTrafficFilter; //TOPORT NETWORK /** * Options passed to the TLS stack @@ -72,14 +132,28 @@ public class ClientOptions { RequireTLS } + public enum ProxyType { + NoProxy, + SystemConfiguredProxy, + SOCKS5Proxy, + HTTPConnectProxy + }; + public ClientOptions() { useStreamCompression = true; useTLS = UseTLS.UseTLSWhenAvailable; - useStreamResumption = false; allowPLAINWithoutTLS = false; + useStreamResumption = false; + forgetPassword = false; useAcks = true; + singleSignOn = false; manualHostname = ""; manualPort = -1; + proxyType = ProxyType.SystemConfiguredProxy; + manualProxyHostname = ""; + manualProxyPort = -1; + boshHTTPConnectProxyAuthID = new SafeByteArray(""); + boshHTTPConnectProxyAuthPassword = new SafeByteArray(""); } @Override diff --git a/src/com/isode/stroke/client/ClientSession.java b/src/com/isode/stroke/client/ClientSession.java index b8ec4a9..42d82ca 100644 --- a/src/com/isode/stroke/client/ClientSession.java +++ b/src/com/isode/stroke/client/ClientSession.java @@ -38,6 +38,8 @@ 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.sasl.DIGESTMD5ClientAuthenticator; +import com.isode.stroke.sasl.EXTERNALClientAuthenticator; import com.isode.stroke.session.SessionStream; import com.isode.stroke.signals.Signal; import com.isode.stroke.signals.Signal1; @@ -54,7 +56,7 @@ import com.isode.stroke.idn.IDNConverter; import com.isode.stroke.idn.ICUConverter; import com.isode.stroke.crypto.CryptoProvider; import com.isode.stroke.crypto.JavaCryptoProvider; - +import java.util.logging.Logger; import java.util.List; import java.util.UUID; @@ -65,6 +67,7 @@ public class ClientSession { public final Signal1 onStanzaReceived = new Signal1(); public final Signal1 onStanzaAcked = new Signal1(); + private Logger logger_ = Logger.getLogger(this.getClass().getName()); private SignalConnection streamElementReceivedConnection; private SignalConnection streamStreamStartReceivedConnection; private SignalConnection streamClosedConnection; @@ -88,8 +91,10 @@ public class ClientSession { private StanzaAckResponder stanzaAckResponder_; private com.isode.stroke.base.Error error_; private CertificateTrustChecker certificateTrustChecker; - private IDNConverter idnConverter = new ICUConverter(); //TOPORT: Accomodated later in Constructor. - private CryptoProvider crypto = new JavaCryptoProvider(); //TOPORT: Accomodated later in Constructor. + private IDNConverter idnConverter; + private CryptoProvider crypto; + private boolean singleSignOn; + private int authenticationPort; public enum State { @@ -126,9 +131,6 @@ public class ClientSession { }; public Error(final Type type) { - if (type == null) { - throw new IllegalStateException(); - } this.type = type; } }; @@ -139,10 +141,12 @@ public class ClientSession { RequireTLS } - private ClientSession(final JID jid, final SessionStream stream) { + private ClientSession(final JID jid, final SessionStream stream, IDNConverter idnConverter, CryptoProvider crypto) { localJID = jid; state = State.Initial; this.stream = stream; + this.idnConverter = idnConverter; + this.crypto = crypto; allowPLAINOverNonTLS = false; useStreamCompression = true; useTLS = UseTLS.UseTLSWhenAvailable; @@ -150,11 +154,15 @@ public class ClientSession { needSessionStart = false; needResourceBind = false; needAcking = false; + rosterVersioningSupported = false; authenticator = null; + certificateTrustChecker = null; + singleSignOn = false; + authenticationPort = -1; } - public static ClientSession create(final JID jid, final SessionStream stream) { - return new ClientSession(jid, stream); + public static ClientSession create(final JID jid, final SessionStream stream, IDNConverter idnConverter, CryptoProvider crypto) { + return new ClientSession(jid, stream, idnConverter, crypto); } public State getState() { @@ -201,6 +209,18 @@ public class ClientSession { certificateTrustChecker = checker; } + public void setSingleSignOn(boolean b) { + singleSignOn = b; + } + + /** + * Sets the port number used in Kerberos authentication + * Does not affect network connectivity. + */ + public void setAuthenticationPort(int i) { + authenticationPort = i; + } + public void start() { streamStreamStartReceivedConnection = stream.onStreamStartReceived.connect(new Slot1(){ public void call(final ProtocolHeader p1) { @@ -308,13 +328,13 @@ public class ClientSession { if (ack.isValid()) { stanzaAckRequester_.handleAckReceived(ack.getHandledStanzasCount()); } - //else { - //logger_.warning("Got invalid ack from server"); /*FIXME: Do we want logging here? - //} + else { + logger_.warning("Got invalid ack from server"); + } + } + else { + logger_.warning("Ignoring ack"); } - //else { - //logger_.warning("Ignoring ack"); /*FIXME: Do we want logging here?*/ - //} } else if (element instanceof StreamError) { finishSession(Error.Type.StreamError); @@ -341,7 +361,7 @@ public class ClientSession { else if (UseTLS.RequireTLS.equals(useTLS) && !stream.isTLSEncrypted()) { finishSession(Error.Type.NoSupportedAuthMechanismsError); } - else if (useStreamCompression && streamFeatures.hasCompressionMethod("zlib")) { + else if (useStreamCompression && stream.supportsZLibCompression() && streamFeatures.hasCompressionMethod("zlib")) { state = State.Compressing; stream.writeElement(new CompressRequest("zlib")); } @@ -349,20 +369,27 @@ public class ClientSession { if (stream.hasTLSCertificate()) { if (streamFeatures.hasAuthenticationMechanism("EXTERNAL")) { state = State.Authenticating; - stream.writeElement(new AuthRequest("EXTERNAL",new SafeByteArray())); + stream.writeElement(new AuthRequest("EXTERNAL",new SafeByteArray(""))); } else { finishSession(Error.Type.TLSClientCertificateError); } } else if (streamFeatures.hasAuthenticationMechanism("EXTERNAL")) { + authenticator = new EXTERNALClientAuthenticator(); state = State.Authenticating; - stream.writeElement(new AuthRequest("EXTERNAL",new SafeByteArray())); + stream.writeElement(new AuthRequest("EXTERNAL",new SafeByteArray(""))); } else if (streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1") || streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1-PLUS")) { - final SCRAMSHA1ClientAuthenticator scramAuthenticator = new SCRAMSHA1ClientAuthenticator(UUID.randomUUID().toString(), streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1-PLUS"), idnConverter, crypto); + ByteArray finishMessage = new ByteArray(); + boolean plus = streamFeatures.hasAuthenticationMechanism("SCRAM-SHA-1-PLUS"); if (stream.isTLSEncrypted()) { - scramAuthenticator.setTLSChannelBindingData(stream.getTLSFinishMessage()); + finishMessage = stream.getTLSFinishMessage(); + plus &= !(finishMessage.isEmpty()); + } + final SCRAMSHA1ClientAuthenticator scramAuthenticator = new SCRAMSHA1ClientAuthenticator(UUID.randomUUID().toString(), plus, idnConverter, crypto); + if (!(finishMessage.isEmpty())) { + scramAuthenticator.setTLSChannelBindingData(finishMessage); } authenticator = scramAuthenticator; state = State.WaitingForCredentials; @@ -373,13 +400,12 @@ public class ClientSession { 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 if (streamFeatures.hasAuthenticationMechanism("DIGEST-MD5") && crypto.isMD5AllowedForCrypto()) { + // FIXME: Host should probably be the actual host + authenticator = new DIGESTMD5ClientAuthenticator(localJID.getDomain(), UUID.randomUUID().toString(), crypto); + state = State.WaitingForCredentials; + onNeedCredentials.emit(); + } else { finishSession(Error.Type.NoSupportedAuthMechanismsError); } @@ -400,7 +426,9 @@ public class ClientSession { } } else if (element instanceof Compressed) { - checkState(State.Compressing); + if(!checkState(State.Compressing)) { + return; + } state = State.WaitingForStreamStart; stream.addZLibCompression(); stream.resetXMPPParser(); @@ -439,7 +467,9 @@ public class ClientSession { } else if (element instanceof AuthChallenge) { final AuthChallenge challenge = (AuthChallenge) element; - checkState(State.Authenticating); + if(!checkState(State.Authenticating)) { + return; + } assert authenticator != null; if (authenticator.setChallenge(challenge.getValue())) { stream.writeElement(new AuthResponse(authenticator.getResponse())); @@ -450,8 +480,11 @@ public class ClientSession { } else if (element instanceof AuthSuccess) { final AuthSuccess authSuccess = (AuthSuccess)element; - checkState(State.Authenticating); - if (authenticator != null && !authenticator.setChallenge(authSuccess.getValue())) { + if(!checkState(State.Authenticating)) { + return; + } + assert(authenticator != null); + if (!authenticator.setChallenge(authSuccess.getValue())) { finishSession(Error.Type.ServerVerificationFailedError); } else { @@ -462,7 +495,6 @@ public class ClientSession { } } else if (element instanceof AuthFailure) { - authenticator = null; finishSession(Error.Type.AuthenticationFailedError); } else if (element instanceof TLSProceed) { @@ -518,6 +550,8 @@ public class ClientSession { if (!checkState(State.WaitingForCredentials)) { throw new IllegalStateException("Asking for credentials when we shouldn't be asked."); } + //assert(WaitingForCredentials); + //assert(authenticator); state = State.Authenticating; authenticator.setCredentials(localJID.getNode(), password); stream.writeElement(new AuthRequest(authenticator.getName(), authenticator.getResponse())); @@ -536,7 +570,7 @@ public class ClientSession { } else { final ServerIdentityVerifier identityVerifier = new ServerIdentityVerifier(localJID, idnConverter); - if (identityVerifier.certificateVerifies(peerCertificate)) { + if (!certificateChain.isEmpty() && identityVerifier.certificateVerifies(peerCertificate)) { continueAfterTLSEncrypted(); } else { @@ -592,20 +626,23 @@ public class ClientSession { } private void finishSession(final Error.Type error) { - Error localError = null; - if (error != null) { - localError = new Error(error); - } - finishSession(localError); + finishSession(new ClientSession.Error(error)); } private void finishSession(final com.isode.stroke.base.Error error) { state = State.Finishing; - error_ = error; + if(error_ == null) { + error_ = error; + } else { + logger_.warning("Session finished twice"); + } assert stream.isOpen(); if (stanzaAckResponder_ != null) { stanzaAckResponder_.handleAckRequestReceived(); } + if (authenticator != null) { + authenticator = null; + } stream.writeFooter(); stream.close(); } diff --git a/src/com/isode/stroke/client/ClientSessionStanzaChannel.java b/src/com/isode/stroke/client/ClientSessionStanzaChannel.java index 0aa024f..e8275a0 100644 --- a/src/com/isode/stroke/client/ClientSessionStanzaChannel.java +++ b/src/com/isode/stroke/client/ClientSessionStanzaChannel.java @@ -9,6 +9,7 @@ package com.isode.stroke.client; import com.isode.stroke.base.Error; +import com.isode.stroke.base.IDGenerator; import com.isode.stroke.elements.IQ; import com.isode.stroke.elements.Message; import com.isode.stroke.elements.Presence; @@ -26,7 +27,7 @@ import java.util.logging.Logger; * StanzaChannel implementation around a ClientSession. */ public class ClientSessionStanzaChannel extends StanzaChannel { - private final IDGenerator idGenerator = new IDGenerator(); + private IDGenerator idGenerator = new IDGenerator(); private ClientSession session; private static final Logger logger_ = Logger.getLogger(ClientSessionStanzaChannel.class.getName()); private SignalConnection sessionInitializedConnection; @@ -34,6 +35,21 @@ public class ClientSessionStanzaChannel extends StanzaChannel { private SignalConnection sessionStanzaReceivedConnection; private SignalConnection sessionStanzaAckedConnection; + protected void finalize() throws Throwable { + try { + if(session != null) { + sessionFinishedConnection.disconnect(); + sessionStanzaReceivedConnection.disconnect(); + sessionStanzaAckedConnection.disconnect(); + sessionInitializedConnection.disconnect(); + session = null; + } + } + finally { + super.finalize(); + } + } + public void setSession(final ClientSession session) { assert this.session == null; this.session = session; diff --git a/src/com/isode/stroke/client/ClientXMLTracer.java b/src/com/isode/stroke/client/ClientXMLTracer.java new file mode 100644 index 0000000..e3a65a6 --- /dev/null +++ b/src/com/isode/stroke/client/ClientXMLTracer.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010-2014 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import com.isode.stroke.client.CoreClient; +import com.isode.stroke.client.XMLBeautifier; +import com.isode.stroke.base.SafeByteArray; +import com.isode.stroke.signals.SignalConnection; +import com.isode.stroke.signals.Slot1; + +public class ClientXMLTracer { + + private XMLBeautifier beautifier; + private boolean bosh; + private SignalConnection onDataReadConnection; + private SignalConnection onDataWrittenConnection; + + public ClientXMLTracer(CoreClient client) { + this(client, false); + } + + public ClientXMLTracer(CoreClient client, boolean bosh) { + this.bosh = bosh; + beautifier = new XMLBeautifier(true, true); + onDataReadConnection = client.onDataRead.connect(new Slot1() { + @Override + public void call(SafeByteArray s) { + printData('<', s); + } + }); + onDataWrittenConnection = client.onDataWritten.connect(new Slot1() { + @Override + public void call(SafeByteArray s) { + printData('>', s); + } + }); + } + + private void printData(char direction, final SafeByteArray data) { + printLine(direction); + if (bosh) { + String line = data.toString(); + // Disabled because it swallows bits of XML (namespaces, if I recall) + // size_t endOfHTTP = line.find("\r\n\r\n"); + // if (false && endOfHTTP != std::string::npos) { + // std::cerr << line.substr(0, endOfHTTP) << std::endl << beautifier->beautify(line.substr(endOfHTTP)) << std::endl; + // } + // else { + System.err.println(line); + // } + } + else { + System.err.println(beautifier.beautify(data.toString())); + } + } + + private void printLine(char c) { + for (int i = 0; i < 80; ++i) { + System.err.println(c); + } + System.err.println(); + } +} diff --git a/src/com/isode/stroke/client/CoreClient.java b/src/com/isode/stroke/client/CoreClient.java index 74ba031..2fc10ae 100644 --- a/src/com/isode/stroke/client/CoreClient.java +++ b/src/com/isode/stroke/client/CoreClient.java @@ -11,7 +11,10 @@ import com.isode.stroke.elements.Presence; import com.isode.stroke.elements.Stanza; import com.isode.stroke.elements.StreamType; import com.isode.stroke.jid.JID; +import com.isode.stroke.crypto.JavaCryptoProvider; +import com.isode.stroke.idn.ICUConverter; import com.isode.stroke.network.Connection; +import com.isode.stroke.network.ConnectionFactory; import com.isode.stroke.network.Connector; import com.isode.stroke.network.DomainNameResolveError; import com.isode.stroke.network.NetworkFactories; @@ -30,6 +33,9 @@ import com.isode.stroke.tls.CertificateTrustChecker; import com.isode.stroke.tls.CertificateVerificationError; import com.isode.stroke.tls.CertificateWithKey; import com.isode.stroke.tls.TLSOptions; +import com.isode.stroke.tls.TLSError; +import java.util.logging.Logger; +import java.util.Vector; /** * The central class for communicating with an XMPP server. @@ -85,7 +91,6 @@ public class CoreClient { 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_; @@ -101,6 +106,12 @@ public class CoreClient { private SignalConnection sessionFinishedConnection_; private SignalConnection sessionNeedCredentialsConnection_; private SignalConnection connectorConnectFinishedConnection_; + private SignalConnection onMessageReceivedConnection; + private SignalConnection onPresenceReceivedConnection; + private SignalConnection onStanzaAckedConnection; + private SignalConnection onAvailableChangedConnection; + private Vector proxyConnectionFactories = new Vector(); + private Logger logger_ = Logger.getLogger(this.getClass().getName()); /** * Constructor. @@ -125,25 +136,25 @@ public class CoreClient { this.networkFactories = networkFactories; this.certificateTrustChecker = null; stanzaChannel_ = new ClientSessionStanzaChannel(); - stanzaChannel_.onMessageReceived.connect(new Slot1() { + onMessageReceivedConnection = stanzaChannel_.onMessageReceived.connect(new Slot1() { public void call(Message p1) { - onMessageReceived.emit(p1); + handleMessageReceived(p1); } }); - stanzaChannel_.onPresenceReceived.connect(new Slot1() { + onPresenceReceivedConnection = stanzaChannel_.onPresenceReceived.connect(new Slot1() { public void call(Presence p1) { - onPresenceReceived.emit(p1); + handlePresenceReceived(p1); } }); - stanzaChannel_.onStanzaAcked.connect(new Slot1() { + onStanzaAckedConnection = stanzaChannel_.onStanzaAcked.connect(new Slot1() { public void call(Stanza p1) { - onStanzaAcked.emit(p1); + handleStanzaAcked(p1); } }); - stanzaChannel_.onAvailableChanged.connect(new Slot1() { + onAvailableChangedConnection = stanzaChannel_.onAvailableChanged.connect(new Slot1() { public void call(Boolean p1) { handleStanzaChannelAvailableChanged(p1); @@ -154,22 +165,29 @@ public class CoreClient { iqRouter_.setJID(jid); } - /*CoreClient::~CoreClient() { - if (session_ || connection_) { - std::cerr << "Warning: Client not disconnected properly" << std::endl; + protected void finalize() throws Throwable { + try { + forceReset(); + onAvailableChangedConnection.disconnect(); + onMessageReceivedConnection.disconnect(); + onPresenceReceivedConnection.disconnect(); + onStanzaAckedConnection.disconnect(); + stanzaChannel_ = null; + } + finally { + super.finalize(); + } } - 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_; - }*/ + /** + * Connects the client to the server. + * + * After the connection is established, the client will set + * initialize the stream and authenticate. + */ + public void connect() { + connect(new ClientOptions()); + } /** * Connect using the standard XMPP connection rules (i.e. SRV then A/AAAA). @@ -177,33 +195,114 @@ public class CoreClient { * @param o Client options to use in the connection, must not be null */ public void connect(final ClientOptions o) { + logger_.fine("Connecting "); forceReset(); disconnectRequested_ = false; assert (connector_ == null); options = o; - /* FIXME: Port Proxies */ + + //TO PORT + /*// Determine connection types to use + assert(proxyConnectionFactories.isEmpty()); + boolean useDirectConnection = true; + HostAddressPort systemSOCKS5Proxy = networkFactories.getProxyProvider().getSOCKS5Proxy(); + HostAddressPort systemHTTPConnectProxy = networkFactories.getProxyProvider().getHTTPConnectProxy(); + switch (o.proxyType) { + case ClientOptions.ProxyType.NoProxy: + logger_.fine(" without a proxy\n"); + break; + case ClientOptions.ProxyType.SystemConfiguredProxy: + logger_.fine(" with a system configured proxy\n"); + if (systemSOCKS5Proxy.isValid()) { + logger_.fine("Found SOCK5 Proxy: " + systemSOCKS5Proxy.getAddress().toString() + ":" + systemSOCKS5Proxy.getPort() + "\n"); + proxyConnectionFactories.add(new SOCKS5ProxiedConnectionFactory(networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory(), systemSOCKS5Proxy.getAddress().toString(), systemSOCKS5Proxy.getPort())); + } + if (systemHTTPConnectProxy.isValid()) { + logger_.fine("Found HTTPConnect Proxy: " + systemHTTPConnectProxy.getAddress().toString() + ":" + systemHTTPConnectProxy.getPort() + "\n"); + proxyConnectionFactories.add(new HTTPConnectProxiedConnectionFactory(networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory(), systemHTTPConnectProxy.getAddress().toString(), systemHTTPConnectProxy.getPort())); + } + break; + case ClientOptions.ProxyType.SOCKS5Proxy: { + logger_.fine(" with manual configured SOCKS5 proxy\n"); + String proxyHostname = o.manualProxyHostname.empty() ? systemSOCKS5Proxy.getAddress().toString() : o.manualProxyHostname; + int proxyPort = o.manualProxyPort == -1 ? systemSOCKS5Proxy.getPort() : o.manualProxyPort; + logger_.fine("Proxy: " + proxyHostname + ":" + proxyPort + "\n"); + proxyConnectionFactories.add(new SOCKS5ProxiedConnectionFactory(networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory(), proxyHostname, proxyPort)); + useDirectConnection = false; + break; + } + case ClientOptions.ProxyType.HTTPConnectProxy: { + logger_.fine(" with manual configured HTTPConnect proxy\n"); + std::string proxyHostname = o.manualProxyHostname.empty() ? systemHTTPConnectProxy.getAddress().toString() : o.manualProxyHostname; + int proxyPort = o.manualProxyPort == -1 ? systemHTTPConnectProxy.getPort() : o.manualProxyPort; + logger_.fine("Proxy: " + proxyHostname + ":" << proxyPort + "\n"); + proxyConnectionFactories.add(new HTTPConnectProxiedConnectionFactory(networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory(), proxyHostname, proxyPort, o.httpTrafficFilter)); + useDirectConnection = false; + break; + } + } + Vector connectionFactories = new Vector(proxyConnectionFactories); + if (useDirectConnection) { + connectionFactories.add(networkFactories.getConnectionFactory()); + }*/ + String host = (o.manualHostname == null || o.manualHostname.isEmpty()) ? jid_.getDomain() : o.manualHostname; int port = o.manualPort; String serviceLookupPrefix = (o.manualHostname == null || o.manualHostname.isEmpty() ? "_xmpp-client._tcp." : null); + assert(connector_ == null); + //TO PORT + /*if (options.boshURL.isEmpty()) { + connector_ = new ChainedConnector(host, port, serviceLookupPrefix, networkFactories.getDomainNameResolver(), connectionFactories, networkFactories.getTimerFactory()); + connector_.onConnectFinished.connect(boost::bind(&CoreClient::handleConnectorFinished, this, _1, _2)); + connector_.setTimeoutMilliseconds(2*60*1000); + connector_.start(); + } + else { + /* Autodiscovery of which proxy works is largely ok with a TCP session, because this is a one-off. With BOSH + * it would be quite painful given that potentially every stanza could be sent on a new connection. + */ + //sessionStream_ = boost::make_shared(boost::make_shared(options.boshURL, networkFactories.getConnectionFactory(), networkFactories.getXMLParserFactory(), networkFactories.getTLSContextFactory()), getPayloadParserFactories(), getPayloadSerializers(), networkFactories.getTLSContextFactory(), networkFactories.getTimerFactory(), networkFactories.getXMLParserFactory(), networkFactories.getEventLoop(), host, options.boshHTTPConnectProxyURL, options.boshHTTPConnectProxyAuthID, options.boshHTTPConnectProxyAuthPassword); + /*sessionStream_ = boost::shared_ptr(new BOSHSessionStream( + options.boshURL, + getPayloadParserFactories(), + getPayloadSerializers(), + networkFactories.getConnectionFactory(), + networkFactories.getTLSContextFactory(), + networkFactories.getTimerFactory(), + networkFactories.getXMLParserFactory(), + networkFactories.getEventLoop(), + networkFactories.getDomainNameResolver(), + host, + options.boshHTTPConnectProxyURL, + options.boshHTTPConnectProxyAuthID, + options.boshHTTPConnectProxyAuthPassword, + options.tlsOptions)); + sessionStream_.onDataRead.connect(boost::bind(&CoreClient::handleDataRead, this, _1)); + sessionStream_.onDataWritten.connect(boost::bind(&CoreClient::handleDataWritten, this, _1)); + bindSessionToStream(); + }*/ connector_ = Connector.create(host, port, serviceLookupPrefix, networkFactories.getDomainNameResolver(), networkFactories.getConnectionFactory(), networkFactories.getTimerFactory()); connectorConnectFinishedConnection_ = connector_.onConnectFinished.connect(new Slot2() { public void call(Connection p1, com.isode.stroke.base.Error p2) { handleConnectorFinished(p1, p2); } }); - connector_.setTimeoutMilliseconds(60 * 1000); + connector_.setTimeoutMilliseconds(2*60*1000); connector_.start(); } private void bindSessionToStream() { - session_ = ClientSession.create(jid_, sessionStream_); + //TO PORT + //session_ = ClientSession::create(jid_, sessionStream_, networkFactories.getIDNConverter(), networkFactories.getCryptoProvider()); + session_ = ClientSession.create(jid_, sessionStream_, new ICUConverter(), new JavaCryptoProvider()); session_.setCertificateTrustChecker(certificateTrustChecker); session_.setUseStreamCompression(options.useStreamCompression); session_.setAllowPLAINOverNonTLS(options.allowPLAINWithoutTLS); + session_.setSingleSignOn(options.singleSignOn); + session_.setAuthenticationPort(options.manualPort); switch (options.useTLS) { case UseTLSWhenAvailable: session_.setUseTLS(ClientSession.UseTLS.UseTLSWhenAvailable); - session_.setCertificateTrustChecker(certificateTrustChecker); break; case NeverUseTLS: session_.setUseTLS(ClientSession.UseTLS.NeverUseTLS); @@ -229,10 +328,13 @@ public class CoreClient { session_.start(); } - void handleConnectorFinished(final Connection connection, final com.isode.stroke.base.Error error) { + private void handleConnectorFinished(final Connection connection, final com.isode.stroke.base.Error error) { resetConnector(); if (connection == null) { + if (options.forgetPassword) { + purgePassword(); + } ClientError clientError = null; if (!disconnectRequested_) { if (error instanceof DomainNameResolveError) { @@ -242,7 +344,8 @@ public class CoreClient { } } onDisconnected.emit(clientError); - } else { + } + else { assert (connection_ == null); connection_ = connection; @@ -284,7 +387,12 @@ public class CoreClient { connector_.stop(); } } - + + /** + * Checks whether the client is active. + * + * A client is active when it is connected or connecting to the server. + */ public boolean isActive() { return (session_ != null && !session_.isFinished()) || connector_ != null; } @@ -308,19 +416,14 @@ public class CoreClient { } private void handleSessionFinished(final com.isode.stroke.base.Error error) { - sessionFinishedConnection_.disconnect(); - sessionNeedCredentialsConnection_.disconnect(); - session_ = null; - - sessionStreamDataReadConnection_.disconnect(); - sessionStreamDataWrittenConnection_.disconnect(); - sessionStream_ = null; - - connection_.disconnect(); - connection_ = null; + if (options.forgetPassword) { + purgePassword(); + } + resetSession(); - ClientError clientError = null; + ClientError actualerror = null; if (error != null) { + ClientError clientError = null; if (error instanceof ClientSession.Error) { ClientSession.Error actualError = (ClientSession.Error) error; switch (actualError.type) { @@ -351,9 +454,23 @@ public class CoreClient { case TLSClientCertificateError: clientError = new ClientError(ClientError.Type.ClientCertificateError); break; - /* Note: no case clause for "StreamError" */ + case StreamError: + clientError = new ClientError(ClientError.Type.StreamError); + break; } - } else if (error instanceof SessionStream.SessionStreamError) { + } + else if (error instanceof TLSError) { + TLSError actualError = (TLSError)error; + switch(actualError.getType()) { + case CertificateCardRemoved: + clientError = new ClientError(ClientError.Type.CertificateCardRemoved); + break; + case UnknownError: + clientError = new ClientError(ClientError.Type.TLSError); + break; + } + } + else if (error instanceof SessionStream.SessionStreamError) { SessionStream.SessionStreamError actualError = (SessionStream.SessionStreamError) error; switch (actualError.type) { case ParseError: @@ -408,19 +525,29 @@ public class CoreClient { case InvalidServerIdentity: clientError = new ClientError(ClientError.Type.InvalidServerIdentityError); break; + case Revoked: + clientError = new ClientError(ClientError.Type.RevokedError); + break; + case RevocationCheckFailed: + clientError = new ClientError(ClientError.Type.RevocationCheckFailedError); + break; } } /* If "error" was non-null, we expect to be able to derive * a non-null "clientError". */ NotNull.exceptIfNull(clientError,"clientError"); + actualerror = clientError; } - onDisconnected.emit(clientError); + onDisconnected.emit(actualerror); } private void handleNeedCredentials() { assert session_ != null; session_.sendCredentials(password_); + if (options.forgetPassword) { + purgePassword(); + } } private void handleDataRead(final SafeByteArray data) { @@ -431,8 +558,26 @@ public class CoreClient { onDataWritten.emit(data); } + private void handlePresenceReceived(Presence presence) { + onPresenceReceived.emit(presence); + } + + private void handleMessageReceived(Message message) { + onMessageReceived.emit(message); + } + + private void handleStanzaAcked(Stanza stanza) { + onStanzaAcked.emit(stanza); + } + + private void purgePassword() { + password_ = new SafeByteArray(); + } + private void handleStanzaChannelAvailableChanged(final boolean available) { if (available) { + iqRouter_.setJID(session_.getLocalJID()); + handleConnected(); onConnected.emit(); } } @@ -446,6 +591,13 @@ public class CoreClient { } /** + * Sends raw, unchecked data. + */ + public void sendData(final String data) { + sessionStream_.writeData(data); + } + + /** * Get the IQRouter responsible for all IQs on this connection. * Use this to send IQs. */ @@ -458,6 +610,8 @@ public class CoreClient { } /** + * Checks whether the client is connected to the server, + * and stanzas can be sent. * @return session is available for sending/receiving stanzas. */ public boolean isAvailable() { @@ -478,12 +632,26 @@ public class CoreClient { connectorConnectFinishedConnection_.disconnect(); } connector_ = null; + //TO PORT + /*for(ConnectionFactory* f, proxyConnectionFactories) { + delete f; + } + proxyConnectionFactories.clear();*/ } protected ClientSession getSession() { return session_; } + protected NetworkFactories getNetworkFactories() { + return networkFactories; + } + + /** + * Called before onConnected signal is emmitted. + */ + protected void handleConnected() {} + private void resetSession() { session_.onFinished.disconnectAll(); session_.onNeedCredentials.disconnectAll(); @@ -494,6 +662,10 @@ public class CoreClient { if (connection_ != null) { connection_.disconnect(); } + // TO PORT + /*else if (sessionStream_ instanceof BOSHSessionStream) { + sessionStream_.close(); + }*/ sessionStream_ = null; connection_ = null; } @@ -512,11 +684,25 @@ public class CoreClient { } } + /** + * Checks whether stream management is enabled. + * + * If stream management is enabled, onStanzaAcked will be + * emitted when a stanza is received by the server. + * + * \see onStanzaAcked + */ + public boolean getStreamManagementEnabled() { + return stanzaChannel_.getStreamManagementEnabled(); + } + private void forceReset() { if (connector_ != null) { + logger_.warning("Client not disconnected properly: Connector still active\n"); resetConnector(); } if (sessionStream_ != null || connection_ != null) { + logger_.warning("Client not disconnected properly: Session still active\n"); resetSession(); } diff --git a/src/com/isode/stroke/client/DummyNickManager.java b/src/com/isode/stroke/client/DummyNickManager.java new file mode 100644 index 0000000..ee7e67b --- /dev/null +++ b/src/com/isode/stroke/client/DummyNickManager.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +public class DummyNickManager extends NickManager { + + public String getOwnNick() { + return ""; + } + + public void setOwnNick(final String nick) { + } +} \ No newline at end of file diff --git a/src/com/isode/stroke/client/IDGenerator.java b/src/com/isode/stroke/client/IDGenerator.java deleted file mode 100644 index c7bb494..0000000 --- a/src/com/isode/stroke/client/IDGenerator.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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/MemoryStorages.java b/src/com/isode/stroke/client/MemoryStorages.java index 16d235d..bafd480 100644 --- a/src/com/isode/stroke/client/MemoryStorages.java +++ b/src/com/isode/stroke/client/MemoryStorages.java @@ -14,8 +14,12 @@ import com.isode.stroke.roster.RosterStorage; import com.isode.stroke.vcards.VCardMemoryStorage; import com.isode.stroke.vcards.VCardStorage; +/** + * An implementation of Storages for storing all + * controller data in memory. + */ public class MemoryStorages implements Storages { - private VCardStorage vcardStorage; + private VCardMemoryStorage vcardStorage; private AvatarStorage avatarStorage; private CapsStorage capsStorage; private RosterStorage rosterStorage; @@ -53,4 +57,8 @@ public class MemoryStorages implements Storages { return avatarStorage; } + /*@Override + public HistoryStorage getHistoryStorage() { + return historyStorage; + }*/ } diff --git a/src/com/isode/stroke/client/NickManagerImpl.java b/src/com/isode/stroke/client/NickManagerImpl.java index 17f9d6b..8a2ebed 100644 --- a/src/com/isode/stroke/client/NickManagerImpl.java +++ b/src/com/isode/stroke/client/NickManagerImpl.java @@ -13,11 +13,12 @@ import com.isode.stroke.vcards.VCardManager; public class NickManagerImpl extends NickManager { private JID ownJID = new JID(); private String ownNick = ""; + private VCardManager vcardManager; private SignalConnection vCardChangedSignal; - NickManagerImpl(final JID ownJID, VCardManager vcardManager) { + public NickManagerImpl(final JID ownJID, VCardManager vcardManager) { this.ownJID = ownJID; - + this.vcardManager = vcardManager; vCardChangedSignal = vcardManager.onVCardChanged.connect(new Slot2() { @Override public void call(JID p1, VCard p2) { @@ -53,7 +54,7 @@ public class NickManagerImpl extends NickManager { if (vcard != null && !vcard.getNickname().isEmpty()) { nick = vcard.getNickname(); } - if (ownNick != nick && nick != null && !nick.equals(ownNick)) { + if (nick != ownNick && nick != null && !nick.equals(ownNick)) { ownNick = nick; onOwnNickChanged.emit(ownNick); } diff --git a/src/com/isode/stroke/client/NickResolver.java b/src/com/isode/stroke/client/NickResolver.java index fe2b129..470e4b9 100644 --- a/src/com/isode/stroke/client/NickResolver.java +++ b/src/com/isode/stroke/client/NickResolver.java @@ -16,14 +16,19 @@ import com.isode.stroke.signals.Slot2; import com.isode.stroke.signals.Slot3; import com.isode.stroke.vcards.VCardManager; +// FIXME: The NickResolver currently relies on the vcard being requested by the client on login. +// The VCardManager should get an onConnected() signal (which is signalled when the stanzachannel is available(, and each time this is emitted, +// the nickresolver should request the vcard. +// FIXME: The ownJID functionality should probably be removed, and NickManager should be used directly. + public class NickResolver { - private JID ownJID_; - private String ownNick_; + private JID ownJID_ = new JID(); + private String ownNick_ = ""; private XMPPRoster xmppRoster_; private MUCRegistry mucRegistry_; private VCardManager vcardManager_; - public final Signal2 onNickChanged = new Signal2(); + public final Signal2 onNickChanged = new Signal2(); public NickResolver(final JID ownJID, XMPPRoster xmppRoster, VCardManager vcardManager, MUCRegistry mucRegistry) { ownJID_ = ownJID; @@ -57,7 +62,7 @@ public class NickResolver { } void handleJIDAdded(final JID jid) { - String oldNick= jidToNick(jid); + String oldNick = jidToNick(jid); onNickChanged.emit(jid, oldNick); } @@ -67,7 +72,7 @@ public class NickResolver { return ownNick_; } } - + String nick = ""; if (mucRegistry_ != null && mucRegistry_.isMUC(jid.toBare()) ) { return jid.getResource().isEmpty() ? jid.toBare().toString() : jid.getResource(); } @@ -83,6 +88,7 @@ public class NickResolver { if (jid.compare(ownJID_, JID.CompareType.WithoutResource) != 0) { return; } + String initialNick = ownNick_; ownNick_ = ownJID_.toString(); if (ownVCard != null) { if (!ownVCard.getNickname().isEmpty()) { diff --git a/src/com/isode/stroke/client/StanzaChannel.java b/src/com/isode/stroke/client/StanzaChannel.java index 5a1d270..253f026 100644 --- a/src/com/isode/stroke/client/StanzaChannel.java +++ b/src/com/isode/stroke/client/StanzaChannel.java @@ -32,7 +32,7 @@ public abstract class StanzaChannel extends IQChannel { public final Signal1 onMessageReceived = new Signal1(); public final Signal1 onPresenceReceived = new Signal1(); - public final Signal1 onAvailableChanged = new Signal1(); + public final Signal1 onAvailableChanged = new Signal1(); public final Signal1 onStanzaAcked = new Signal1(); diff --git a/src/com/isode/stroke/client/Storages.java b/src/com/isode/stroke/client/Storages.java index 25821e5..24ffff4 100644 --- a/src/com/isode/stroke/client/Storages.java +++ b/src/com/isode/stroke/client/Storages.java @@ -9,6 +9,10 @@ import com.isode.stroke.disco.CapsStorage; import com.isode.stroke.roster.RosterStorage; import com.isode.stroke.vcards.VCardStorage; +/** + * An interface to hold storage classes for different + * controllers. + */ public interface Storages { VCardStorage getVCardStorage(); AvatarStorage getAvatarStorage(); diff --git a/src/com/isode/stroke/client/XMLBeautifier.java b/src/com/isode/stroke/client/XMLBeautifier.java new file mode 100644 index 0000000..7f6e346 --- /dev/null +++ b/src/com/isode/stroke/client/XMLBeautifier.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ +/* + * Copyright (c) 2014-2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import java.util.Stack; +import com.isode.stroke.parser.XMLParserClient; +import com.isode.stroke.parser.XMLParser; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.PlatformXMLParserFactory; + +public class XMLBeautifier implements XMLParserClient { + + private boolean doIndention; + private boolean doColoring; + + private int intLevel; + private String inputBuffer = ""; + private StringBuffer buffer = new StringBuffer(); + private XMLParser parser; + + private boolean lastWasStepDown; + private Stack parentNSs = new Stack(); + + // all bold but reset + public static final String colorReset = "\u001B[0m"; + public static final String ANSI_BLACK = "\u001B[30m"; + public static final String colorRed = "\u001B[31m"; + public static final String colorGreen = "\u001B[32m"; + public static final String colorYellow = "\u001B[33m"; + public static final String colorBlue = "\u001B[34m"; + public static final String ANSI_PURPLE = "\u001B[35m"; + public static final String colorCyan = "\u001B[36m"; + public static final String ANSI_WHITE = "\u001B[37m"; + + public XMLBeautifier(boolean indention, boolean coloring) { + this.doIndention = indention; + this.doColoring = coloring; + intLevel = 0; + parser = null; + lastWasStepDown = false; + } + + public String beautify(final String text) { + parser = PlatformXMLParserFactory.createXMLParser(this); + intLevel = 0; + buffer.append(""); + parser.parse(text); + parser = null; + return buffer.toString(); + } + + public void handleStartElement(final String element, final String ns, final AttributeMap attributes) { + if (doIndention) { + if (intLevel != 0) buffer.append("\n"); + } + indent(); + buffer.append("<").append(doColoring ? styleTag(element) : element); + if (!ns.isEmpty() && (!parentNSs.isEmpty() && !parentNSs.peek().equals(ns))) { + buffer.append(" "); + buffer.append((doColoring ? styleAttribute("xmlns") : "xmlns")); + buffer.append("="); + buffer.append("\"").append((doColoring ? styleNamespace(ns) : ns)).append("\""); + } + if (!attributes.getEntries().isEmpty()) { + for(AttributeMap.Entry entry : attributes.getEntries()) { + buffer.append(" "); + buffer.append((doColoring ? styleAttribute(entry.getAttribute().getName()) : entry.getAttribute().getName())); + buffer.append("="); + buffer.append("\"").append((doColoring ? styleValue(entry.getValue()) : entry.getValue())).append("\""); + } + } + buffer.append(">"); + ++intLevel; + lastWasStepDown = false; + parentNSs.push(ns); + } + + public void handleEndElement(final String element, final String ns) { + --intLevel; + parentNSs.pop(); + if (/*hadCDATA.top() ||*/ lastWasStepDown) { + if (doIndention) { + buffer.append("\n"); + } + indent(); + } + buffer.append(""); + lastWasStepDown = true; + } + + public void handleCharacterData(final String data) { + buffer.append(data); + lastWasStepDown = false; + } + + private void indent() { + for (int i = 0; i < intLevel; ++i) { + buffer.append(" "); + } + } + + private String styleTag(final String text) { + String result = ""; + result += colorYellow; + result += text; + result += colorReset; + return result; + } + + private String styleNamespace(final String text) { + String result = ""; + result += colorRed; + result += text; + result += colorReset; + return result; + } + + private String styleAttribute(final String text) { + String result = ""; + result += colorGreen; + result += text; + result += colorReset; + return result; + } + + private String styleValue(final String text) { + String result = ""; + result += colorCyan; + result += text; + result += colorReset; + return result; + } +} \ No newline at end of file diff --git a/src/com/isode/stroke/crypto/JavaCryptoProvider.java b/src/com/isode/stroke/crypto/JavaCryptoProvider.java index 1353225..98eee89 100644 --- a/src/com/isode/stroke/crypto/JavaCryptoProvider.java +++ b/src/com/isode/stroke/crypto/JavaCryptoProvider.java @@ -139,6 +139,6 @@ public class JavaCryptoProvider extends CryptoProvider { @Override public boolean isMD5AllowedForCrypto() { - return false; + return true; } } \ No newline at end of file diff --git a/src/com/isode/stroke/disco/ClientDiscoManager.java b/src/com/isode/stroke/disco/ClientDiscoManager.java index ba620ed..52a31f2 100644 --- a/src/com/isode/stroke/disco/ClientDiscoManager.java +++ b/src/com/isode/stroke/disco/ClientDiscoManager.java @@ -69,7 +69,7 @@ public class ClientDiscoManager { * Called when the client is connected. This resets the presence sender, * such that it assumes initial presence hasn't been sent yet. */ - void handleConnected() { + public void handleConnected() { presenceSender.reset(); } diff --git a/test/com/isode/stroke/client/ClientBlockListManagerTest.java b/test/com/isode/stroke/client/ClientBlockListManagerTest.java new file mode 100644 index 0000000..2ec75bc --- /dev/null +++ b/test/com/isode/stroke/client/ClientBlockListManagerTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.Before; +import com.isode.stroke.elements.BlockPayload; +import com.isode.stroke.elements.BlockListPayload; +import com.isode.stroke.elements.UnblockPayload; +import com.isode.stroke.client.StanzaChannel; +import com.isode.stroke.client.DummyStanzaChannel; +import com.isode.stroke.client.ClientBlockListManager; +import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.queries.GenericRequest; +import com.isode.stroke.jid.JID; +import java.util.Vector; + +public class ClientBlockListManagerTest { + + private JID ownJID_ = new JID(); + private IQRouter iqRouter_; + private DummyStanzaChannel stanzaChannel_; + private ClientBlockListManager clientBlockListManager_; + + private void helperInitialBlockListFetch(final Vector blockedJids) { + BlockList blockList = clientBlockListManager_.requestBlockList(); + assertNotNull(blockList); + + // check for IQ request + IQ request = stanzaChannel_.getStanzaAtIndex(new IQ(), 0); + assertNotNull(request); + BlockListPayload requestPayload = request.getPayload(new BlockListPayload()); + assertNotNull(requestPayload); + + assertEquals(BlockList.State.Requesting, blockList.getState()); + assertEquals(BlockList.State.Requesting, clientBlockListManager_.getBlockList().getState()); + + // build IQ response + BlockListPayload responsePayload = new BlockListPayload(); + for(final JID jid : blockedJids) { + responsePayload.addItem(jid); + } + + IQ response = IQ.createResult(ownJID_, new JID(), request.getID(), responsePayload); + stanzaChannel_.sendIQ(response); + stanzaChannel_.onIQReceived.emit(response); + + assertEquals(BlockList.State.Available, clientBlockListManager_.getBlockList().getState()); + assertEquals(responsePayload.getItems(), clientBlockListManager_.getBlockList().getItems()); + } + + @Before + public void setUp() { + ownJID_ = new JID("kev@wonderland.lit"); + stanzaChannel_ = new DummyStanzaChannel(); + iqRouter_ = new IQRouter(stanzaChannel_); + iqRouter_.setJID(ownJID_); + clientBlockListManager_ = new ClientBlockListManager(iqRouter_); + } + + @Test + public void testFetchBlockList() { + Vector blockJids = new Vector(); + blockJids.add(new JID("romeo@montague.net")); + blockJids.add(new JID("iago@shakespeare.lit")); + helperInitialBlockListFetch(blockJids); + + assertEquals(2, clientBlockListManager_.getBlockList().getItems().size()); + } + + @Test + public void testBlockCommand() { + // start with an already fetched block list + Vector vec = new Vector(); + vec.add(new JID("iago@shakespeare.lit")); + helperInitialBlockListFetch(vec); + + assertEquals(1, clientBlockListManager_.getBlockList().getItems().size()); + assertEquals(BlockList.State.Available, clientBlockListManager_.getBlockList().getState()); + + GenericRequest blockRequest = clientBlockListManager_.createBlockJIDRequest(new JID("romeo@montague.net")); + blockRequest.send(); + IQ request = stanzaChannel_.getStanzaAtIndex(new IQ(), 2); + assertNotNull(request); + BlockPayload blockPayload = request.getPayload(new BlockPayload()); + assertNotNull(blockPayload); + assertEquals(new JID("romeo@montague.net"), blockPayload.getItems().get(0)); + + IQ blockRequestResponse = IQ.createResult(request.getFrom(), new JID(), request.getID()); + stanzaChannel_.sendIQ(blockRequestResponse); + stanzaChannel_.onIQReceived.emit(blockRequestResponse); + + assertEquals((1), clientBlockListManager_.getBlockList().getItems().size()); + + // send block push + BlockPayload pushPayload = new BlockPayload(); + pushPayload.addItem(new JID("romeo@montague.net")); + IQ blockPush = IQ.createRequest(IQ.Type.Set, ownJID_, "push1", pushPayload); + stanzaChannel_.sendIQ(blockPush); + stanzaChannel_.onIQReceived.emit(blockPush); + + Vector blockedJIDs = clientBlockListManager_.getBlockList().getItems(); + assertTrue(blockedJIDs.contains(new JID("romeo@montague.net"))); + } + + @Test + public void testUnblockCommand() { + // start with an already fetched block list + Vector initialBlockList = new Vector(); + initialBlockList.add(new JID("iago@shakespeare.lit")); + initialBlockList.add(new JID("romeo@montague.net")); + helperInitialBlockListFetch(initialBlockList); + + assertEquals((2), clientBlockListManager_.getBlockList().getItems().size()); + assertEquals(BlockList.State.Available, clientBlockListManager_.getBlockList().getState()); + + GenericRequest unblockRequest = clientBlockListManager_.createUnblockJIDRequest(new JID("romeo@montague.net")); + unblockRequest.send(); + IQ request = stanzaChannel_.getStanzaAtIndex(new IQ(), 2); + assertNotNull(request); + UnblockPayload unblockPayload = request.getPayload(new UnblockPayload()); + assertNotNull(unblockPayload); + assertEquals(new JID("romeo@montague.net"), unblockPayload.getItems().get(0)); + + IQ unblockRequestResponse = IQ.createResult(request.getFrom(), new JID(), request.getID()); + stanzaChannel_.sendIQ(unblockRequestResponse); + stanzaChannel_.onIQReceived.emit(unblockRequestResponse); + + assertEquals((2), clientBlockListManager_.getBlockList().getItems().size()); + + // send block push + UnblockPayload pushPayload = new UnblockPayload(); + pushPayload.addItem(new JID("romeo@montague.net")); + IQ unblockPush = IQ.createRequest(IQ.Type.Set, ownJID_, "push1", pushPayload); + stanzaChannel_.sendIQ(unblockPush); + stanzaChannel_.onIQReceived.emit(unblockPush); + + Vector blockedJIDs = clientBlockListManager_.getBlockList().getItems(); + assertFalse(blockedJIDs.contains(new JID("romeo@montague.net"))); + } + + @Test + public void testUnblockAllCommand() { + // start with an already fetched block list + Vector initialBlockList = new Vector(); + initialBlockList.add(new JID("iago@shakespeare.lit")); + initialBlockList.add(new JID("romeo@montague.net")); + initialBlockList.add(new JID("benvolio@montague.net")); + helperInitialBlockListFetch(initialBlockList); + + assertEquals((3), clientBlockListManager_.getBlockList().getItems().size()); + assertEquals(BlockList.State.Available, clientBlockListManager_.getBlockList().getState()); + + GenericRequest unblockRequest = clientBlockListManager_.createUnblockAllRequest(); + unblockRequest.send(); + IQ request = stanzaChannel_.getStanzaAtIndex(new IQ(), 2); + assertNotNull(request); + UnblockPayload unblockPayload = request.getPayload(new UnblockPayload()); + assertNotNull(unblockPayload); + assertEquals(true, unblockPayload.getItems().isEmpty()); + + IQ unblockRequestResponse = IQ.createResult(request.getFrom(), new JID(), request.getID()); + stanzaChannel_.sendIQ(unblockRequestResponse); + stanzaChannel_.onIQReceived.emit(unblockRequestResponse); + + assertEquals((3), clientBlockListManager_.getBlockList().getItems().size()); + + // send block push + UnblockPayload pushPayload = new UnblockPayload(); + IQ unblockPush = IQ.createRequest(IQ.Type.Set, ownJID_, "push1", pushPayload); + stanzaChannel_.sendIQ(unblockPush); + stanzaChannel_.onIQReceived.emit(unblockPush); + + assertEquals(true, clientBlockListManager_.getBlockList().getItems().isEmpty()); + } +} \ No newline at end of file diff --git a/test/com/isode/stroke/client/ClientSessionTest.java b/test/com/isode/stroke/client/ClientSessionTest.java new file mode 100644 index 0000000..75ad504 --- /dev/null +++ b/test/com/isode/stroke/client/ClientSessionTest.java @@ -0,0 +1,871 @@ +/* + * Copyright (c) 2010-2014 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.Before; +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.base.SafeByteArray; +import com.isode.stroke.session.SessionStream; +import com.isode.stroke.elements.AuthChallenge; +import com.isode.stroke.elements.AuthFailure; +import com.isode.stroke.elements.AuthRequest; +import com.isode.stroke.elements.Message; +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.sasl.DIGESTMD5ClientAuthenticator; +import com.isode.stroke.sasl.EXTERNALClientAuthenticator; +import com.isode.stroke.session.SessionStream; +import com.isode.stroke.client.ClientSession; +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 com.isode.stroke.tls.SimpleCertificate; +import com.isode.stroke.tls.BlindCertificateTrustChecker; +import com.isode.stroke.idn.IDNConverter; +import com.isode.stroke.idn.ICUConverter; +import com.isode.stroke.crypto.CryptoProvider; +import com.isode.stroke.crypto.JavaCryptoProvider; +import java.util.logging.Logger; +import java.util.List; +import java.util.UUID; +import java.util.Vector; +import java.util.Collection; + +public class ClientSessionTest { + + private IDNConverter idnConverter; + private MockSessionStream server; + private boolean sessionFinishedReceived; + private boolean needCredentials; + private com.isode.stroke.base.Error sessionFinishedError; + private BlindCertificateTrustChecker blindCertificateTrustChecker; + private CryptoProvider crypto; + + private class MockSessionStream extends SessionStream { + + class Event { + public Event(Element element) { + this.element = element; + this.footer = false; + } + public Event(final ProtocolHeader header) { + this.header = header; + this.footer = false; + } + public Event() { + this.footer = true; + } + + public Element element; + public ProtocolHeader header; + public boolean footer; + }; + + public MockSessionStream() { + this.available = true; + this.canTLSEncrypt = true; + this.tlsEncrypted = false; + this.compressed = false; + this.whitespacePingEnabled = false; + this.resetCount = 0; + } + + public void disconnect() { + + } + + public void close() { + onClosed.emit((com.isode.stroke.base.Error)null); + } + + public boolean isOpen() { + return available; + } + + public void writeHeader(final ProtocolHeader header) { + receivedEvents.add(new Event(header)); + } + + public void writeFooter() { + receivedEvents.add(new Event()); + } + + public void writeElement(Element element) { + receivedEvents.add(new Event(element)); + } + + public void writeData(final String data) { + } + + public boolean supportsTLSEncryption() { + return canTLSEncrypt; + } + + public void addTLSEncryption() { + tlsEncrypted = true; + } + + public boolean isTLSEncrypted() { + return tlsEncrypted; + } + + public ByteArray getTLSFinishMessage() { + return new ByteArray(); + } + + public Certificate getPeerCertificate() { + return new SimpleCertificate(); + } + + public Vector getPeerCertificateChain() { + return new Vector(); + } + + public CertificateVerificationError getPeerCertificateVerificationError() { + return null; + } + + public boolean supportsZLibCompression() { + return true; + } + + public void addZLibCompression() { + compressed = true; + } + + public void setWhitespacePingEnabled(boolean enabled) { + whitespacePingEnabled = enabled; + } + + public void resetXMPPParser() { + resetCount++; + } + + public void breakConnection() { + onClosed.emit(new SessionStream.SessionStreamError(SessionStream.SessionStreamError.Type.ConnectionReadError)); + } + + public void breakTLS() { + onClosed.emit(new SessionStream.SessionStreamError(SessionStream.SessionStreamError.Type.TLSError)); + } + + + public void sendStreamStart() { + ProtocolHeader header = new ProtocolHeader(); + header.setTo("foo.com"); + onStreamStartReceived.emit(header); + } + + public void sendStreamFeaturesWithStartTLS() { + StreamFeatures streamFeatures = new StreamFeatures(); + streamFeatures.setHasStartTLS(); + onElementReceived.emit(streamFeatures); + } + + public void sendChallenge() { + onElementReceived.emit(new AuthChallenge()); + } + + public void sendStreamError() { + onElementReceived.emit(new StreamError()); + } + + public void sendTLSProceed() { + onElementReceived.emit(new TLSProceed()); + } + + public void sendTLSFailure() { + onElementReceived.emit(new StartTLSFailure()); + } + + public void sendStreamFeaturesWithMultipleAuthentication() { + StreamFeatures streamFeatures = new StreamFeatures(); + streamFeatures.addAuthenticationMechanism("PLAIN"); + streamFeatures.addAuthenticationMechanism("DIGEST-MD5"); + streamFeatures.addAuthenticationMechanism("SCRAM-SHA1"); + onElementReceived.emit(streamFeatures); + } + + public void sendStreamFeaturesWithPLAINAuthentication() { + StreamFeatures streamFeatures = new StreamFeatures(); + streamFeatures.addAuthenticationMechanism("PLAIN"); + onElementReceived.emit(streamFeatures); + } + + public void sendStreamFeaturesWithEXTERNALAuthentication() { + StreamFeatures streamFeatures = new StreamFeatures(); + streamFeatures.addAuthenticationMechanism("EXTERNAL"); + onElementReceived.emit(streamFeatures); + } + + public void sendStreamFeaturesWithUnknownAuthentication() { + StreamFeatures streamFeatures = new StreamFeatures(); + streamFeatures.addAuthenticationMechanism("UNKNOWN"); + onElementReceived.emit(streamFeatures); + } + + public void sendStreamFeaturesWithBindAndStreamManagement() { + StreamFeatures streamFeatures = new StreamFeatures(); + streamFeatures.setHasResourceBind(); + streamFeatures.setHasStreamManagement(); + onElementReceived.emit(streamFeatures); + } + + public void sendEmptyStreamFeatures() { + onElementReceived.emit(new StreamFeatures()); + } + + public void sendAuthSuccess() { + onElementReceived.emit(new AuthSuccess()); + } + + public void sendAuthFailure() { + onElementReceived.emit(new AuthFailure()); + } + + public void sendStreamManagementEnabled() { + onElementReceived.emit(new StreamManagementEnabled()); + } + + public void sendStreamManagementFailed() { + onElementReceived.emit(new StreamManagementFailed()); + } + + public void sendBindResult() { + ResourceBind resourceBind = new ResourceBind(); + resourceBind.setJID(new JID("foo@bar.com/bla")); + IQ iq = IQ.createResult(new JID("foo@bar.com"), bindID, resourceBind); + onElementReceived.emit(iq); + } + + public void sendMessage() { + Message message = new Message(); + message.setTo(new JID("foo@bar.com/bla")); + onElementReceived.emit(message); + } + + public void receiveStreamStart() { + Event event = popEvent(); + assertNotNull(event.header); + } + + public void receiveStartTLS() { + Event event = popEvent(); + assertNotNull(event.element); + assertTrue(event.element instanceof StartTLSRequest); + } + + public void receiveAuthRequest(final String mech) { + Event event = popEvent(); + assertNotNull(event.element); + AuthRequest request = (AuthRequest)(event.element); + assertNotNull(request); + assertEquals(mech, request.getMechanism()); + } + + public void receiveStreamManagementEnable() { + Event event = popEvent(); + assertNotNull(event.element); + assertTrue(event.element instanceof EnableStreamManagement); + } + + public void receiveBind() { + Event event = popEvent(); + assertNotNull(event.element); + IQ iq = (IQ)(event.element); + assertNotNull(iq); + assertNotNull(iq.getPayload(new ResourceBind())); + bindID = iq.getID(); + } + + public void receiveAck(int n) { + Event event = popEvent(); + assertNotNull(event.element); + StanzaAck ack = (StanzaAck)(event.element); + assertNotNull(ack); + assertEquals(n, ack.getHandledStanzasCount()); + } + + public Event popEvent() { + assertFalse(receivedEvents.isEmpty()); + Event event = receivedEvents.firstElement(); + receivedEvents.remove(receivedEvents.firstElement()); + return event; + } + + public boolean available; + public boolean canTLSEncrypt; + public boolean tlsEncrypted; + public boolean compressed; + public boolean whitespacePingEnabled; + public String bindID = ""; + public int resetCount; + public Vector receivedEvents = new Vector(); + }; + + public ClientSession createSession() { + ClientSession session = ClientSession.create(new JID("me@foo.com"), server, idnConverter, crypto); + session.onFinished.connect(new Slot1() { + @Override + public void call(com.isode.stroke.base.Error e) { + handleSessionFinished(e); + } + }); + session.onNeedCredentials.connect(new Slot() { + @Override + public void call() { + handleSessionNeedCredentials(); + } + }); + session.setAllowPLAINOverNonTLS(true); + return session; + } + + public void initializeSession(ClientSession session) { + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithPLAINAuthentication(); + session.sendCredentials(new SafeByteArray("mypass")); + server.receiveAuthRequest("PLAIN"); + server.sendAuthSuccess(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithBindAndStreamManagement(); + server.receiveBind(); + server.sendBindResult(); + server.receiveStreamManagementEnable(); + server.sendStreamManagementEnabled(); + } + + public void handleSessionFinished(com.isode.stroke.base.Error error) { + sessionFinishedReceived = true; + sessionFinishedError = error; + } + + public void handleSessionNeedCredentials() { + needCredentials = true; + } + + @Before + public void setUp() { + crypto = new JavaCryptoProvider(); + idnConverter = new ICUConverter(); + server = new MockSessionStream(); + sessionFinishedReceived = false; + needCredentials = false; + blindCertificateTrustChecker = new BlindCertificateTrustChecker(); + } + + @Test + public void testStart_Error() { + ClientSession session = createSession(); + session.start(); + server.breakConnection(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testStart_StreamError() { + ClientSession session = createSession(); + session.start(); + server.sendStreamStart(); + server.sendStreamError(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testStartTLS() { + ClientSession session = createSession(); + session.setCertificateTrustChecker(blindCertificateTrustChecker); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithStartTLS(); + server.receiveStartTLS(); + assertFalse(server.tlsEncrypted); + server.sendTLSProceed(); + assertTrue(server.tlsEncrypted); + server.onTLSEncrypted.emit(); + server.receiveStreamStart(); + server.sendStreamStart(); + + session.finish(); + } + + @Test + public void testStartTLS_ServerError() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithStartTLS(); + server.receiveStartTLS(); + server.sendTLSFailure(); + + assertFalse(server.tlsEncrypted); + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testStartTLS_ConnectError() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithStartTLS(); + server.receiveStartTLS(); + server.sendTLSProceed(); + server.breakTLS(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testStartTLS_InvalidIdentity() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithStartTLS(); + server.receiveStartTLS(); + assertFalse(server.tlsEncrypted); + server.sendTLSProceed(); + assertTrue(server.tlsEncrypted); + server.onTLSEncrypted.emit(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + assertEquals(CertificateVerificationError.Type.InvalidServerIdentity, ((CertificateVerificationError)(sessionFinishedError)).getType()); + } + + @Test + public void testStart_StreamFeaturesWithoutResourceBindingFails() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendEmptyStreamFeatures(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testAuthenticate() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithPLAINAuthentication(); + assertTrue(needCredentials); + assertEquals(ClientSession.State.WaitingForCredentials, session.getState()); + session.sendCredentials(new SafeByteArray("mypass")); + server.receiveAuthRequest("PLAIN"); + server.sendAuthSuccess(); + server.receiveStreamStart(); + + session.finish(); + } + + @Test + public void testAuthenticate_Unauthorized() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithPLAINAuthentication(); + assertTrue(needCredentials); + assertEquals(ClientSession.State.WaitingForCredentials, session.getState()); + session.sendCredentials(new SafeByteArray("mypass")); + server.receiveAuthRequest("PLAIN"); + server.sendAuthFailure(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testAuthenticate_PLAINOverNonTLS() { + ClientSession session = createSession(); + session.setAllowPLAINOverNonTLS(false); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithPLAINAuthentication(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testAuthenticate_RequireTLS() { + ClientSession session = createSession(); + session.setUseTLS(ClientSession.UseTLS.RequireTLS); + session.setAllowPLAINOverNonTLS(true); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithMultipleAuthentication(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testAuthenticate_NoValidAuthMechanisms() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithUnknownAuthentication(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testAuthenticate_EXTERNAL() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithEXTERNALAuthentication(); + server.receiveAuthRequest("EXTERNAL"); + server.sendAuthSuccess(); + server.receiveStreamStart(); + + session.finish(); + } + + @Test + public void testUnexpectedChallenge() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithEXTERNALAuthentication(); + server.receiveAuthRequest("EXTERNAL"); + server.sendChallenge(); + server.sendChallenge(); + + assertEquals(ClientSession.State.Finished, session.getState()); + assertTrue(sessionFinishedReceived); + assertNotNull(sessionFinishedError); + } + + @Test + public void testStreamManagement() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithPLAINAuthentication(); + session.sendCredentials(new SafeByteArray("mypass")); + server.receiveAuthRequest("PLAIN"); + server.sendAuthSuccess(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithBindAndStreamManagement(); + server.receiveBind(); + server.sendBindResult(); + server.receiveStreamManagementEnable(); + server.sendStreamManagementEnabled(); + + assertTrue(session.getStreamManagementEnabled()); + // TODO: Test if the requesters & responders do their work + assertEquals(ClientSession.State.Initialized, session.getState()); + + session.finish(); + } + + @Test + public void testStreamManagement_Failed() { + ClientSession session = createSession(); + session.start(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithPLAINAuthentication(); + session.sendCredentials(new SafeByteArray("mypass")); + server.receiveAuthRequest("PLAIN"); + server.sendAuthSuccess(); + server.receiveStreamStart(); + server.sendStreamStart(); + server.sendStreamFeaturesWithBindAndStreamManagement(); + server.receiveBind(); + server.sendBindResult(); + server.receiveStreamManagementEnable(); + server.sendStreamManagementFailed(); + + assertFalse(session.getStreamManagementEnabled()); + assertEquals(ClientSession.State.Initialized, session.getState()); + + session.finish(); + } + + @Test + public void testFinishAcksStanzas() { + ClientSession session = createSession(); + initializeSession(session); + server.sendMessage(); + server.sendMessage(); + server.sendMessage(); + + session.finish(); + + server.receiveAck(3); + } + + /*void testAuthenticate() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + session->onNeedCredentials.connect(boost::bind(&ClientSessionTest::setNeedCredentials, this)); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithAuthentication(); + session->startSession(); + processEvents(); + CPPUNIT_ASSERT_EQUAL(ClientSession::WaitingForCredentials, session->getState()); + CPPUNIT_ASSERT(needCredentials_); + + getMockServer()->expectAuth("me", "mypass"); + getMockServer()->sendAuthSuccess(); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + session->sendCredentials("mypass"); + CPPUNIT_ASSERT_EQUAL(ClientSession::Authenticating, session->getState()); + processEvents(); + CPPUNIT_ASSERT_EQUAL(ClientSession::Negotiating, session->getState()); + } + + void testAuthenticate_Unauthorized() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithAuthentication(); + session->startSession(); + processEvents(); + + getMockServer()->expectAuth("me", "mypass"); + getMockServer()->sendAuthFailure(); + session->sendCredentials("mypass"); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::Error, session->getState()); + CPPUNIT_ASSERT_EQUAL(ClientSession::AuthenticationFailedError, *session->getError()); + } + + void testAuthenticate_NoValidAuthMechanisms() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithUnsupportedAuthentication(); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::Error, session->getState()); + CPPUNIT_ASSERT_EQUAL(ClientSession::NoSupportedAuthMechanismsError, *session->getError()); + } + + void testResourceBind() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithResourceBind(); + getMockServer()->expectResourceBind("Bar", "session-bind"); + // FIXME: Check CPPUNIT_ASSERT_EQUAL(ClientSession::BindingResource, session->getState()); + getMockServer()->sendResourceBindResponse("me@foo.com/Bar", "session-bind"); + session->startSession(); + + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::SessionStarted, session->getState()); + CPPUNIT_ASSERT_EQUAL(JID("me@foo.com/Bar"), session->getLocalJID()); + } + + void testResourceBind_ChangeResource() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithResourceBind(); + getMockServer()->expectResourceBind("Bar", "session-bind"); + getMockServer()->sendResourceBindResponse("me@foo.com/Bar123", "session-bind"); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::SessionStarted, session->getState()); + CPPUNIT_ASSERT_EQUAL(JID("me@foo.com/Bar123"), session->getLocalJID()); + } + + void testResourceBind_EmptyResource() { + boost::shared_ptr session(createSession("me@foo.com")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithResourceBind(); + getMockServer()->expectResourceBind("", "session-bind"); + getMockServer()->sendResourceBindResponse("me@foo.com/NewResource", "session-bind"); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::SessionStarted, session->getState()); + CPPUNIT_ASSERT_EQUAL(JID("me@foo.com/NewResource"), session->getLocalJID()); + } + + void testResourceBind_Error() { + boost::shared_ptr session(createSession("me@foo.com")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithResourceBind(); + getMockServer()->expectResourceBind("", "session-bind"); + getMockServer()->sendError("session-bind"); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::Error, session->getState()); + CPPUNIT_ASSERT_EQUAL(ClientSession::ResourceBindError, *session->getError()); + } + + void testSessionStart() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + session->onSessionStarted.connect(boost::bind(&ClientSessionTest::setSessionStarted, this)); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithSession(); + getMockServer()->expectSessionStart("session-start"); + // FIXME: Check CPPUNIT_ASSERT_EQUAL(ClientSession::StartingSession, session->getState()); + getMockServer()->sendSessionStartResponse("session-start"); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::SessionStarted, session->getState()); + CPPUNIT_ASSERT(sessionStarted_); + } + + void testSessionStart_Error() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithSession(); + getMockServer()->expectSessionStart("session-start"); + getMockServer()->sendError("session-start"); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::Error, session->getState()); + CPPUNIT_ASSERT_EQUAL(ClientSession::SessionStartError, *session->getError()); + } + + void testSessionStart_AfterResourceBind() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + session->onSessionStarted.connect(boost::bind(&ClientSessionTest::setSessionStarted, this)); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeaturesWithResourceBindAndSession(); + getMockServer()->expectResourceBind("Bar", "session-bind"); + getMockServer()->sendResourceBindResponse("me@foo.com/Bar", "session-bind"); + getMockServer()->expectSessionStart("session-start"); + getMockServer()->sendSessionStartResponse("session-start"); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(ClientSession::SessionStarted, session->getState()); + CPPUNIT_ASSERT(sessionStarted_); + } + + void testWhitespacePing() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeatures(); + session->startSession(); + processEvents(); + CPPUNIT_ASSERT(session->getWhitespacePingLayer()); + } + + void testReceiveElementAfterSessionStarted() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeatures(); + session->startSession(); + processEvents(); + + getMockServer()->expectMessage(); + session->sendElement(boost::make_shared())); + } + + void testSendElement() { + boost::shared_ptr session(createSession("me@foo.com/Bar")); + session->onElementReceived.connect(boost::bind(&ClientSessionTest::addReceivedElement, this, _1)); + getMockServer()->expectStreamStart(); + getMockServer()->sendStreamStart(); + getMockServer()->sendStreamFeatures(); + getMockServer()->sendMessage(); + session->startSession(); + processEvents(); + + CPPUNIT_ASSERT_EQUAL(1, static_cast(receivedElements_.size())); + CPPUNIT_ASSERT(boost::dynamic_pointer_cast(receivedElements_[0])); + }*/ +} \ No newline at end of file diff --git a/test/com/isode/stroke/client/NickResolverTest.java b/test/com/isode/stroke/client/NickResolverTest.java new file mode 100644 index 0000000..afe3706 --- /dev/null +++ b/test/com/isode/stroke/client/NickResolverTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2010-2013 Isode Limited. + * All right reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.Before; +import com.isode.stroke.elements.VCard; +import com.isode.stroke.elements.RosterItemPayload; +import com.isode.stroke.client.StanzaChannel; +import com.isode.stroke.client.DummyStanzaChannel; +import com.isode.stroke.client.ClientBlockListManager; +import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.queries.GenericRequest; +import com.isode.stroke.jid.JID; +import com.isode.stroke.crypto.CryptoProvider; +import com.isode.stroke.crypto.JavaCryptoProvider; +import com.isode.stroke.vcards.VCardMemoryStorage; +import com.isode.stroke.vcards.VCardManager; +import com.isode.stroke.vcards.VCardStorage; +import com.isode.stroke.roster.XMPPRosterImpl; +import com.isode.stroke.muc.MUCRegistry; +import com.isode.stroke.client.NickResolver; +import java.util.Vector; +import java.util.Collection; + +public class NickResolverTest { + + private Collection groups_ = new Vector(); + private XMPPRosterImpl xmppRoster_; + private VCardStorage vCardStorage_; + private IQRouter iqRouter_; + private DummyStanzaChannel stanzaChannel_; + private VCardManager vCardManager_; + private MUCRegistry registry_; + private NickResolver resolver_; + private JID ownJID_; + private CryptoProvider crypto; + + public void populateOwnVCard(final String nick, final String given, final String full) { + VCard vcard = new VCard(); + if (!nick.isEmpty()) { + vcard.setNickname(nick); + } + if (!given.isEmpty()) { + vcard.setGivenName(given); + } + if (!full.isEmpty()) { + vcard.setFullName(full); + } + vCardManager_.requestVCard(ownJID_); + IQ result = IQ.createResult(new JID(), stanzaChannel_.sentStanzas.get(0).getID(), vcard); + stanzaChannel_.onIQReceived.emit(result); + } + + @Before + public void setUp() { + crypto = new JavaCryptoProvider(); + ownJID_ = new JID("kev@wonderland.lit"); + xmppRoster_ = new XMPPRosterImpl(); + stanzaChannel_ = new DummyStanzaChannel(); + iqRouter_ = new IQRouter(stanzaChannel_); + vCardStorage_ = new VCardMemoryStorage(crypto); + vCardManager_ = new VCardManager(ownJID_, iqRouter_, vCardStorage_); + registry_ = new MUCRegistry(); + resolver_ = new NickResolver(ownJID_, xmppRoster_, vCardManager_, registry_); + } + + @Test + public void testMUCNick() { + registry_.addMUC(new JID("foo@bar")); + JID testJID = new JID("foo@bar/baz"); + + assertEquals(("baz"), resolver_.jidToNick(testJID)); + } + + @Test + public void testMUCNoNick() { + registry_.addMUC(new JID("foo@bar")); + JID testJID = new JID("foo@bar"); + + assertEquals(("foo@bar"), resolver_.jidToNick(testJID)); + } + + @Test + public void testNoMatch() { + JID testJID = new JID("foo@bar/baz"); + + assertEquals(("foo@bar"), resolver_.jidToNick(testJID)); + } + + @Test + public void testZeroLengthMatch() { + JID testJID = new JID("foo@bar/baz"); + xmppRoster_.addContact(testJID, "", groups_, RosterItemPayload.Subscription.Both); + assertEquals(("foo@bar"), resolver_.jidToNick(testJID)); + } + + @Test + public void testMatch() { + JID testJID = new JID("foo@bar/baz"); + xmppRoster_.addContact(testJID, "Test", groups_, RosterItemPayload.Subscription.Both); + + assertEquals(("Test"), resolver_.jidToNick(testJID)); + } + + @Test + public void testOverwrittenMatch() { + JID testJID = new JID("foo@bar/baz"); + xmppRoster_.addContact(testJID, "FailTest", groups_, RosterItemPayload.Subscription.Both); + xmppRoster_.addContact(testJID, "Test", groups_, RosterItemPayload.Subscription.Both); + + assertEquals(("Test"), resolver_.jidToNick(testJID)); + } + + @Test + public void testRemovedMatch() { + JID testJID = new JID("foo@bar/baz"); + xmppRoster_.addContact(testJID, "FailTest", groups_, RosterItemPayload.Subscription.Both); + xmppRoster_.removeContact(testJID); + assertEquals(("foo@bar"), resolver_.jidToNick(testJID)); + } + + @Test + public void testOwnNickFullOnly() { + populateOwnVCard("", "", "Kevin Smith"); + assertEquals(("Kevin Smith"), resolver_.jidToNick(ownJID_)); + } + + @Test + public void testOwnNickGivenAndFull() { + populateOwnVCard("", "Kevin", "Kevin Smith"); + assertEquals(("Kevin"), resolver_.jidToNick(ownJID_)); + } + + @Test + public void testOwnNickNickEtAl() { + populateOwnVCard("Kev", "Kevin", "Kevin Smith"); + assertEquals(("Kev"), resolver_.jidToNick(ownJID_)); + } +} \ No newline at end of file -- cgit v0.10.2-6-g49f6