summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--PortingProgress.txt4
-rw-r--r--src/com/isode/stroke/network/BOSHConnection.java527
-rw-r--r--src/com/isode/stroke/network/BOSHConnectionPool.java417
-rw-r--r--src/com/isode/stroke/parser/BOSHBodyExtractor.java174
-rw-r--r--src/com/isode/stroke/parser/PlatformXMLParserFactory.java9
-rw-r--r--src/com/isode/stroke/streamstack/DummyStreamLayer.java51
-rw-r--r--test/com/isode/stroke/network/BOSHConnectionPoolTest.java509
-rw-r--r--test/com/isode/stroke/network/BoshConnectionTest.java352
-rw-r--r--test/com/isode/stroke/parser/BOSHBodyExtractorTest.java101
9 files changed, 2139 insertions, 5 deletions
diff --git a/PortingProgress.txt b/PortingProgress.txt
index d8dee4f..4eb2e15 100644
--- a/PortingProgress.txt
+++ b/PortingProgress.txt
@@ -133,7 +133,6 @@ Network:
All files ported to 6ca201d0b48f4273e24dd7bff17c4a46eeaddf39 except for:
-BOSHConnection, BOSHConnectionPool, BOSHConnectionPoolTest, BOSHConnectionTest -- Not Yet Ported!
DomainNameServiceQuery -- sortResults not Implemented.
DomainNameServiceQueryTest -- Not Yet Ported!
GConfProxyProvider, UnixProxyProvider, WindowsProxyProvider, MacOSXProxyProvider -- Not Yet Ported!
@@ -150,8 +149,7 @@ Parser:
All files ported to 6ca201d0b48f4273e24dd7bff17c4a46eeaddf39 except for:
-BOSHBodyExtractor, BOSHBodyExtractorTest, ExpatParser,
-IdleParserTest, LibXMLParser, WhiteboardParser -- Not Yet Ported!
+ExpatParser, IdleParserTest, LibXMLParser, WhiteboardParser -- Not Yet Ported!
-----
Presence:
diff --git a/src/com/isode/stroke/network/BOSHConnection.java b/src/com/isode/stroke/network/BOSHConnection.java
new file mode 100644
index 0000000..43b990d
--- /dev/null
+++ b/src/com/isode/stroke/network/BOSHConnection.java
@@ -0,0 +1,527 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.network;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.base.Error;
+import com.isode.stroke.base.SafeByteArray;
+import com.isode.stroke.base.URL;
+import com.isode.stroke.network.BOSHConnection.BOSHError.Type;
+import com.isode.stroke.parser.BOSHBodyExtractor;
+import com.isode.stroke.parser.BOSHBodyExtractor.BOSHBody;
+import com.isode.stroke.parser.XMLParserFactory;
+import com.isode.stroke.session.SessionStream.SessionStreamError;
+import com.isode.stroke.signals.Signal1;
+import com.isode.stroke.signals.Signal2;
+import com.isode.stroke.signals.SignalConnection;
+import com.isode.stroke.signals.Slot;
+import com.isode.stroke.signals.Slot1;
+import com.isode.stroke.signals.Slot2;
+import com.isode.stroke.streamstack.DummyStreamLayer;
+import com.isode.stroke.streamstack.HighLayer;
+import com.isode.stroke.streamstack.TLSLayer;
+import com.isode.stroke.tls.Certificate;
+import com.isode.stroke.tls.CertificateVerificationError;
+import com.isode.stroke.tls.CertificateWithKey;
+import com.isode.stroke.tls.TLSContextFactory;
+import com.isode.stroke.tls.TLSError;
+import com.isode.stroke.tls.TLSOptions;
+
+public class BOSHConnection {
+
+ private final URL boshURL_;
+ private Connector connector_;
+ private final XMLParserFactory parserFactory_;
+ private Connection connection_;
+ private final HighLayer dummyLayer_;
+ private final TLSLayer tlsLayer_;
+ private String sid_ = "";
+ private boolean waitingForStartResponse_ = false;
+ private long rid_ = 0;
+ private final SafeByteArray buffer_ = new SafeByteArray();
+ private boolean pending_ = false;
+ private boolean connectionReady_ = false;
+
+ public final Signal1<Boolean> onConnectionFinished = new Signal1<Boolean>();
+ public final Signal1<Boolean> onDisconnected = new Signal1<Boolean>();
+ public final Signal1<BOSHError> onSessionTerminated = new Signal1<BOSHError>();
+ public final Signal2<String, Integer> onSessionStarted = new Signal2<String, Integer>();
+ public final Signal1<SafeByteArray> onXMPPDataRead = new Signal1<SafeByteArray>();
+ public final Signal1<SafeByteArray> onBOSHDataRead = new Signal1<SafeByteArray>();
+ public final Signal1<SafeByteArray> onBOSHDataWritten = new Signal1<SafeByteArray>();
+ public final Signal1<String> onHTTPError = new Signal1<String>();
+
+ private final Logger logger = Logger.getLogger(this.getClass().getName());
+ private SignalConnection onConnectFinishedConnection;
+
+ public static class BOSHError extends com.isode.stroke.session.SessionStream.SessionStreamError {
+
+ public enum Type {
+ BadRequest, HostGone, HostUnknown, ImproperAddressing,
+ InternalServerError, ItemNotFound, OtherRequest, PolicyViolation,
+ RemoteConnectionFailed,RemoteStreamError,SeeOtherURI,SystemShutdown,
+ UndefinedCondition,NoError;
+ }
+
+ public BOSHError(Type type) {
+ super(SessionStreamError.Type.ConnectionReadError);
+ type_ = type;
+ }
+
+ public Type getType() {
+ return type_;
+ }
+
+ private final Type type_;
+
+ }
+
+ public static class Pair<T1,T2> {
+ public final T1 first;
+ public final T2 second;
+
+ public Pair(T1 first,T2 second) {
+ this.first = first;
+ this.second = second;
+ }
+ }
+
+ private BOSHConnection(URL url,Connector connector,XMLParserFactory parserFactory,
+ TLSContextFactory tlsContextFactory,TLSOptions tlsOptions) {
+ boshURL_ = url;
+ connector_ = connector;
+ parserFactory_ = parserFactory;
+ if ("https".equals(boshURL_.getScheme())) {
+ tlsLayer_ = new TLSLayer(tlsContextFactory, tlsOptions);
+ dummyLayer_ = new DummyStreamLayer(tlsLayer_);
+ }
+ else {
+ tlsLayer_ = null;
+ dummyLayer_ = null;
+ }
+ }
+
+ public static BOSHConnection create(URL url,Connector connector,
+ XMLParserFactory parserFactory,TLSContextFactory tlsContextFactory,TLSOptions tlsOptions) {
+ return new BOSHConnection(url, connector, parserFactory, tlsContextFactory, tlsOptions);
+ }
+
+ public void connect() {
+ onConnectFinishedConnection = connector_.onConnectFinished.connect(new Slot2<Connection, Error>() {
+
+ @Override
+ public void call(Connection connection, Error error) {
+ handleConnectFinished(connection);
+ }
+
+ });
+ connector_.start();
+ }
+
+ public void disconnect() {
+ if (connection_ != null) {
+ connection_.disconnect();
+ sid_ = "";
+ }
+ else {
+ handleDisconnected(null);
+ }
+ }
+
+ public void write(SafeByteArray data) {
+ write(data,false,false);
+ }
+
+ private void write(SafeByteArray data, boolean streamRestart, boolean terminate) {
+ assert(connectionReady_);
+ assert(!sid_.isEmpty());
+
+ SafeByteArray safeHeader =
+ createHTTPRequest(data, streamRestart, terminate, rid_, sid_, boshURL_).first;
+
+ onBOSHDataWritten.emit(safeHeader);
+ writeData(safeHeader);
+ pending_ = true;
+
+ String logMessage = "write data: " + safeHeader.toString() + "\n";
+ logger.log(Level.FINE,logMessage);
+ }
+
+ public String getSID() {
+ return sid_;
+ }
+
+ public void setRID(long rid) {
+ rid_ = rid;
+ }
+
+ public void setSID(String sid) {
+ sid_ = sid;
+ }
+
+ public void startStream(String to,long rid) {
+ assert(connectionReady_);
+
+ String content = "<body content='text/xml; charset=utf-8'"
+ + " hold='1'"
+ + " to='" + to + "'"
+ + " rid='" + rid + "'"
+ + " ver='1.6'"
+ + " wait='60'"
+ + " xml:lang='en'"
+ + " xmlns:xmpp='urn:xmpp:bosh'"
+ + " xmpp:version='1.0'"
+ + " xmlns='http://jabber.org/protocol/httpbind' />";
+
+ StringBuilder headerBuilder = new StringBuilder("POST " + boshURL_.getPath() + " HTTP/1.1\r\n"
+ + "Host: " + boshURL_.getHost());
+ Integer boshPort = boshURL_.getPort();
+ if (boshPort != null) {
+ headerBuilder.append(":");
+ headerBuilder.append(boshPort);
+ }
+ headerBuilder.append("\r\n");
+ headerBuilder.append("Accept-Encoding: deflate\r\n");
+ headerBuilder.append("Content-Type: text/xml; charset=utf-8\r\n");
+ headerBuilder.append("Content-Length: ");
+ headerBuilder.append(content.length());
+ headerBuilder.append("\r\n\r\n");
+ headerBuilder.append(content);
+
+ waitingForStartResponse_ = true;
+ SafeByteArray safeHeader = new SafeByteArray(headerBuilder.toString());
+ onBOSHDataWritten.emit(safeHeader);
+ writeData(safeHeader);
+ logger.fine("write stream header: "+safeHeader.toString()+"\n");
+ }
+
+ public void terminateStream() {
+ write(new SafeByteArray(),false,true);
+ }
+
+ public boolean isReadyToSend() {
+ // Without pipelining you need to not send more without first receiving the response
+ // With pipelining you can. Assuming we can't, here
+ return connectionReady_ && !pending_ && !waitingForStartResponse_ && !sid_.isEmpty();
+ }
+
+ public void restartStream() {
+ write(new SafeByteArray(),true,false);
+ }
+
+ public boolean setClientCertificate(CertificateWithKey cert) {
+ if (tlsLayer_ != null) {
+ logger.fine("set client certificate\n");
+ return tlsLayer_.setClientCertificate(cert);
+ }
+ else {
+ return false;
+ }
+ }
+
+ public Certificate getPeerCertificate() {
+ if (tlsLayer_ != null) {
+ return tlsLayer_.getPeerCertificate();
+ }
+ return null;
+ }
+
+ public CertificateVerificationError getPeerCertificateVerficationError() {
+ if (tlsLayer_ != null) {
+ return tlsLayer_.getPeerCertificateVerificationError();
+ }
+ return null;
+ }
+
+ public List<Certificate> getPeerCertificateChain() {
+ if (tlsLayer_ != null) {
+ return tlsLayer_.getPeerCertificateChain();
+ }
+ return new ArrayList<Certificate>();
+ }
+
+ protected static Pair<SafeByteArray,Integer> createHTTPRequest(SafeByteArray data,
+ boolean streamRestart,boolean terminate,long rid,String sid,URL boshURL) {
+ int size = 0;
+ StringBuilder contentBuilder = new StringBuilder();
+ SafeByteArray contentTail = new SafeByteArray("</body>");
+ StringBuilder headerBuilder = new StringBuilder();
+
+ contentBuilder.append("<body rid='");
+ contentBuilder.append(rid);
+ contentBuilder.append("' sid='");
+ contentBuilder.append(sid);
+ contentBuilder.append("'");
+ if (streamRestart) {
+ contentBuilder.append(" xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'");
+ }
+ if (terminate) {
+ contentBuilder.append(" type='terminate'");
+ }
+ contentBuilder.append(" xmlns='http://jabber.org/protocol/httpbind'>");
+
+ SafeByteArray safeContent = new SafeByteArray(contentBuilder.toString());
+ safeContent.append(data);
+ safeContent.append(contentTail);
+
+ size = safeContent.getSize();
+
+ headerBuilder.append("POST " + boshURL.getPath() + " HTTP/1.1\r\n"
+ + "Host: " + boshURL.getHost());
+ Integer boshPort = boshURL.getPort();
+ if (boshPort != null) {
+ headerBuilder.append(":");
+ headerBuilder.append(boshPort);
+ }
+ headerBuilder.append("\r\n");
+ headerBuilder.append("Accept-Encoding: deflate\r\n");
+ headerBuilder.append("Content-Type: text/xml; charset=utf-8\r\n");
+ headerBuilder.append("Content-Length: ");
+ headerBuilder.append(size);
+ headerBuilder.append("\r\n\r\n");
+
+ SafeByteArray safeHeader = new SafeByteArray(headerBuilder.toString());
+ safeHeader.append(safeContent);
+
+ return new Pair<SafeByteArray, Integer>(safeHeader, size);
+ }
+
+ private void handleConnectFinished(Connection connection) {
+ cancelConnector();
+ connectionReady_ = (connection != null);
+ if (connectionReady_) {
+ connection_ = connection;
+ if (tlsLayer_ != null) {
+ connection_.onDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleRawDataRead(data);
+ }
+
+ });
+ connection_.onDisconnected.connect(new Slot1<Connection.Error>() {
+
+ @Override
+ public void call(
+ com.isode.stroke.network.Connection.Error error) {
+ handleDisconnected(error);
+ }
+
+ });
+
+ tlsLayer_.getContext().onDataForNetwork.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleTLSNetworkDataWriteRequest(data);
+ }
+
+ });
+ tlsLayer_.getContext().onDataForApplication.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleTLSApplicationDataRead(data);
+ }
+
+ });
+
+ tlsLayer_.onConnected.connect(new Slot() {
+
+ @Override
+ public void call() {
+ handleTLSConnected();
+ }
+
+ });
+
+ tlsLayer_.onError.connect(new Slot1<TLSError>() {
+
+ @Override
+ public void call(TLSError error) {
+ handleTLSError(error);
+ }
+
+ });
+ tlsLayer_.connect();
+ }
+ else {
+ connection_.onDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleDataRead(data);
+ }
+
+ });
+ connection_.onDisconnected.connect(new Slot1<Connection.Error>() {
+
+ @Override
+ public void call(Connection.Error error) {
+ handleDisconnected(error);
+ }
+
+ });
+ }
+
+ if (!connectionReady_ || tlsLayer_ == null) {
+ onConnectionFinished.emit(!connectionReady_);
+ }
+ }
+ }
+
+ private void handleDataRead(SafeByteArray data) {
+ onBOSHDataRead.emit(data);
+ buffer_.append(data);
+ String response = buffer_.toString();
+ if (!response.contains("\r\n\r\n")) {
+ onBOSHDataRead.emit(new SafeByteArray("[[Previous read incomplete, pending]]"));
+ return;
+ }
+ int httpCodeIndex = response.indexOf(" ")+1;
+ String httpCode = response.substring(httpCodeIndex,httpCodeIndex+3);
+ if (!"200".equals(httpCode)) {
+ onHTTPError.emit(httpCode);
+ return;
+ }
+ ByteArray boshData = new ByteArray(response.substring(response.indexOf("\r\n\r\n")));
+ BOSHBodyExtractor parser = new BOSHBodyExtractor(parserFactory_, boshData);
+ BOSHBody boshBody = parser.getBody();
+ if (boshBody != null) {
+ String typeAttribute = boshBody.getAttributes().getAttribute("type");
+ if ( "terminate".equals(typeAttribute) ) {
+ String conditionAttribute = boshBody.getAttributes().getAttribute("condition");
+ BOSHError.Type errorType = parseTerminationCondition(conditionAttribute);
+ onSessionTerminated.emit(errorType == BOSHError.Type.NoError ? null : new BOSHError(errorType));
+ }
+ buffer_.clear();
+ if (waitingForStartResponse_) {
+ waitingForStartResponse_ = false;
+ sid_ = boshBody.getAttributes().getAttribute("sid");
+ String requestsString = boshBody.getAttributes().getAttribute("requests");
+ Integer requests = Integer.valueOf(2);
+ if (requestsString != null && !requestsString.isEmpty()) {
+ try {
+ requests = Integer.valueOf(requestsString);
+ } catch (NumberFormatException e) {
+ requests = Integer.valueOf(2);
+ }
+ }
+ onSessionStarted.emit(sid_, requests);
+ }
+
+ SafeByteArray payload = new SafeByteArray(boshBody.getContent());
+ /* Say we're good to go again, so don't add anything after here in the method */
+ pending_ = false;
+ onXMPPDataRead.emit(payload);
+ }
+ }
+
+ private void handleDisconnected(Connection.Error error) {
+ cancelConnector();
+ onDisconnected.emit(error != null ? Boolean.TRUE : Boolean.FALSE);
+ sid_ = "";
+ connectionReady_ = false;
+ }
+
+ private Type parseTerminationCondition(String text) {
+ Type condition = Type.UndefinedCondition;
+ if ("bad-request".equals(text)) {
+ condition = Type.BadRequest;
+ }
+ else if ("host-gone".equals(text)) {
+ condition = Type.HostGone;
+ }
+ else if ("host-unknown".equals(text)) {
+ condition = Type.HostUnknown;
+ }
+ else if ("improper-addressing".equals(text)) {
+ condition = Type.ImproperAddressing;
+ }
+ else if ("internal-server-error".equals(text)) {
+ condition = Type.InternalServerError;
+ }
+ else if ("item-not-found".equals(text)) {
+ condition = Type.ItemNotFound;
+ }
+ else if ("other-request".equals(text)) {
+ condition = Type.OtherRequest;
+ }
+ else if ("policy-violation".equals(text)) {
+ condition = Type.PolicyViolation;
+ }
+ else if ("remote-connection-failed".equals(text)) {
+ condition = Type.RemoteConnectionFailed;
+ }
+ else if ("remote-stream-error".equals(text)) {
+ condition = Type.RemoteStreamError;
+ }
+ else if ("see-other-uri".equals(text)) {
+ condition = Type.SeeOtherURI;
+ }
+ else if ("system-shutdown".equals(text)) {
+ condition = Type.SystemShutdown;
+ }
+ else if ("".equals(text)) {
+ condition = Type.NoError;
+ }
+ return condition;
+ }
+
+ private void cancelConnector() {
+ if (connector_ != null) {
+ if (onConnectFinishedConnection != null) {
+ onConnectFinishedConnection.disconnect();
+ }
+ connector_.stop();
+ connector_ = null;
+ }
+ }
+
+ private void handleTLSConnected() {
+ logger.fine("\n");
+ onConnectionFinished.emit(Boolean.FALSE);
+ }
+
+ private void handleTLSApplicationDataRead(SafeByteArray data) {
+ logger.fine("\n");
+ handleDataRead(new SafeByteArray(data));
+ }
+
+ private void handleTLSNetworkDataWriteRequest(SafeByteArray data) {
+ logger.fine("\n");
+ connection_.write(data);
+ }
+
+ private void handleRawDataRead(SafeByteArray data) {
+ logger.fine("\n");
+ tlsLayer_.handleDataRead(data);
+ }
+
+ private void handleTLSError(TLSError error) {
+ // Empty Method
+ }
+
+ private void writeData(SafeByteArray data) {
+ if (tlsLayer_ != null) {
+ tlsLayer_.writeData(data);
+ }
+ else {
+ connection_.write(data);
+ }
+ }
+
+}
diff --git a/src/com/isode/stroke/network/BOSHConnectionPool.java b/src/com/isode/stroke/network/BOSHConnectionPool.java
new file mode 100644
index 0000000..9eefb83
--- /dev/null
+++ b/src/com/isode/stroke/network/BOSHConnectionPool.java
@@ -0,0 +1,417 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.network;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.isode.stroke.base.SafeByteArray;
+import com.isode.stroke.base.SafeString;
+import com.isode.stroke.base.URL;
+import com.isode.stroke.eventloop.EventLoop;
+import com.isode.stroke.network.BOSHConnection.BOSHError;
+import com.isode.stroke.parser.XMLParserFactory;
+import com.isode.stroke.signals.Signal;
+import com.isode.stroke.signals.Signal1;
+import com.isode.stroke.signals.SignalConnection;
+import com.isode.stroke.signals.Slot1;
+import com.isode.stroke.signals.Slot2;
+import com.isode.stroke.tls.Certificate;
+import com.isode.stroke.tls.CertificateVerificationError;
+import com.isode.stroke.tls.CertificateWithKey;
+import com.isode.stroke.tls.TLSContextFactory;
+import com.isode.stroke.tls.TLSOptions;
+
+public class BOSHConnectionPool {
+
+ private final URL boshURL_;
+ private ConnectionFactory connectionFactory_;
+ private final XMLParserFactory xmlParserFactory_;
+ private final TimerFactory timerFactory_;
+ private final List<BOSHConnection> connections_ = new ArrayList<BOSHConnection>();
+ private final Map<BOSHConnection, Set<SignalConnection>> connectionsSignalConnections_
+ = new HashMap<BOSHConnection, Set<SignalConnection>>();
+ private String sid_ = "";
+ private long rid_;
+ private final List<SafeByteArray> dataQueue_ = new ArrayList<SafeByteArray>();
+ private boolean pendingTerminate_;
+ private String to_;
+ private int requestLimit_;
+ private int restartCount_;
+ private boolean pendingRestart_;
+ private List<ConnectionFactory> myConnectionFactories_;
+ private final CachingDomainNameResolver resolver_;
+ private CertificateWithKey clientCertificate_;
+ private TLSContextFactory tlsContextFactory_;
+ private TLSOptions tlsOptions_;
+ private final List<Certificate> pinnedCertificateChain_ = new ArrayList<Certificate>();
+ private CertificateVerificationError lastVerificationError_;
+
+ public final Signal1<BOSHError> onSessionTerminated = new Signal1<BOSHError>();
+ public final Signal onSessionStarted = new Signal();
+ public final Signal1<SafeByteArray> onXMPPDataRead = new Signal1<SafeByteArray>();
+ public final Signal1<SafeByteArray> onBOSHDataRead = new Signal1<SafeByteArray>();
+ public final Signal1<SafeByteArray> onBOSHDataWritten = new Signal1<SafeByteArray>();
+
+ private final Logger logger = Logger.getLogger(this.getClass().getName());
+
+ public BOSHConnectionPool(URL boshURL,DomainNameResolver resolver,
+ ConnectionFactory connectionFactory, XMLParserFactory parserFactory,
+ TLSContextFactory tlsFactory, TimerFactory timerFactory, EventLoop eventLoop,
+ String to,long initialRID,URL boshHTTPConnectProxyURL,
+ SafeString boshHTTPConnectProxyAuthID,SafeString boshHTTPConnectProxyAuthPassword,
+ TLSOptions tlsOptions) {
+ this(boshURL, resolver, connectionFactory, parserFactory, tlsFactory, timerFactory,
+ eventLoop, to, initialRID, boshHTTPConnectProxyURL, boshHTTPConnectProxyAuthID,
+ boshHTTPConnectProxyAuthPassword, tlsOptions, null);
+ }
+
+ public BOSHConnectionPool(URL boshURL,DomainNameResolver realResolver,
+ ConnectionFactory connectionFactory, XMLParserFactory parserFactory,
+ TLSContextFactory tlsFactory, TimerFactory timerFactory, EventLoop eventLoop,
+ String to,long initialRID,URL boshHTTPConnectProxyURL,
+ SafeString boshHTTPConnectProxyAuthID,SafeString boshHTTPConnectProxyAuthPassword,
+ TLSOptions tlsOptions,HTTPTrafficFilter trafficFilter) {
+ boshURL_ = boshURL;
+ connectionFactory_ = connectionFactory;
+ xmlParserFactory_ = parserFactory;
+ timerFactory_ = timerFactory;
+ rid_ = initialRID;
+ pendingTerminate_ = false;
+ to_ = to;
+ requestLimit_ = 2;
+ restartCount_ = 0;
+ pendingRestart_ = false;
+ tlsContextFactory_ = tlsFactory;
+ tlsOptions_ = tlsOptions;
+ if (!boshHTTPConnectProxyURL.isEmpty()) {
+ this.connectionFactory_ =
+ new HTTPConnectProxiedConnectionFactory(realResolver, connectionFactory,
+ timerFactory, boshHTTPConnectProxyURL.getHost(),
+ URL.getPortOrDefaultPort(boshHTTPConnectProxyURL),
+ boshHTTPConnectProxyAuthID.getData(),
+ boshHTTPConnectProxyAuthPassword.getData(), trafficFilter);
+ }
+ resolver_ = new CachingDomainNameResolver(realResolver, eventLoop);
+ }
+
+ public void open() {
+ createConnection();
+ }
+
+ public void write(SafeByteArray data) {
+ dataQueue_.add(data);
+ tryToSendQueuedData();
+ }
+
+ public void writeFooter() {
+ pendingTerminate_ = true;
+ tryToSendQueuedData();
+ }
+
+ public void close() {
+ if (!sid_.isEmpty()) {
+ writeFooter();
+ }
+ else {
+ pendingTerminate_ = true;
+ List<BOSHConnection> connectionCopies = new ArrayList<BOSHConnection>(connections_);
+ for(BOSHConnection connection : connectionCopies) {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+ }
+
+ public void restartStream() {
+ BOSHConnection connection = getSuitableConnection();
+ if (connection != null) {
+ pendingRestart_ = false;
+ rid_++;
+ connection.setRID(rid_);
+ connection.restartStream();
+ restartCount_++;
+ }
+ else {
+ pendingRestart_ = true;
+ }
+ }
+
+ public void setTLSCertificate(CertificateWithKey certWithKey) {
+ clientCertificate_ = certWithKey;
+ }
+
+ public boolean isTLSEncrypted() {
+ return !pinnedCertificateChain_.isEmpty();
+ }
+
+ public Certificate getPeerCertificate() {
+ Certificate peerCertificate = null;
+ if (!pinnedCertificateChain_.isEmpty()) {
+ peerCertificate = pinnedCertificateChain_.get(0);
+ }
+ return peerCertificate;
+ }
+
+
+ public List<Certificate> getPeerCertificateChain() {
+ return new ArrayList<Certificate>(pinnedCertificateChain_);
+ }
+
+ public CertificateVerificationError getPeerCertificateVerificationError() {
+ return lastVerificationError_;
+ }
+
+ private void handleDataRead(SafeByteArray data) {
+ onXMPPDataRead.emit(data);
+ tryToSendQueuedData(); // Will rebalance the connections
+ }
+
+ private void handleSessionStarted(String sid, int requests) {
+ sid_ = sid;
+ requestLimit_ = requests;
+ onSessionStarted.emit();
+ }
+
+ private void handleBOSHDataRead(SafeByteArray data) {
+ onBOSHDataRead.emit(data);
+ }
+
+ private void handleBOSHDataWritten(SafeByteArray data) {
+ onBOSHDataWritten.emit(data);
+ }
+
+ private void handleSessionTerminated(BOSHError error) {
+ onSessionTerminated.emit(error);
+ }
+
+ private void handleConnectFinished(boolean error, BOSHConnection connection) {
+ if (error) {
+ onSessionTerminated.emit(new BOSHError(BOSHError.Type.UndefinedCondition));
+ }
+ else {
+ if ((connection.getPeerCertificate() != null) && pinnedCertificateChain_.isEmpty()) {
+ pinnedCertificateChain_.clear();
+ pinnedCertificateChain_.addAll(connection.getPeerCertificateChain());
+ }
+ if (!pinnedCertificateChain_.isEmpty()) {
+ lastVerificationError_ = connection.getPeerCertificateVerficationError();
+ }
+
+ if (sid_.isEmpty()) {
+ connection.startStream(to_, rid_);
+ }
+ if (pendingRestart_) {
+ restartStream();
+ }
+ tryToSendQueuedData();
+ }
+ }
+
+ private void handleConnectionDisconnected(boolean error, BOSHConnection connection) {
+ destroyConnection(connection);
+ if (pendingTerminate_ && sid_.isEmpty() && connections_.isEmpty()) {
+ handleSessionTerminated(null);
+ }
+ else {
+ /* We might have just freed up a connection slot to send with */
+ tryToSendQueuedData();
+ }
+ }
+
+ private void handleHTTPError(String errorCode) {
+ handleSessionTerminated(new BOSHError(BOSHError.Type.UndefinedCondition));
+ }
+
+ private BOSHConnection createConnection() {
+ Connector connector = Connector.create(boshURL_.getHost(),
+ URL.getPortOrDefaultPort(boshURL_), null, resolver_,
+ connectionFactory_, timerFactory_);
+ final BOSHConnection connection = BOSHConnection.create(boshURL_, connector,
+ xmlParserFactory_, tlsContextFactory_, tlsOptions_);
+ Set<SignalConnection> signalConnections = new HashSet<SignalConnection>();
+ signalConnections.add(connection.onXMPPDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleDataRead(data);
+ }
+
+ }));
+ signalConnections.add(connection.onSessionStarted.connect(new Slot2<String, Integer>() {
+
+ @Override
+ public void call(String sid, Integer requests) {
+ handleSessionStarted(sid, requests.intValue());
+ }
+
+ }));
+ signalConnections.add(connection.onBOSHDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleBOSHDataRead(data);
+ }
+
+ }));
+ signalConnections.add(connection.onBOSHDataWritten.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleBOSHDataWritten(data);
+ }
+
+ }));
+ signalConnections.add(connection.onDisconnected.connect(new Slot1<Boolean>() {
+
+ @Override
+ public void call(Boolean wasError) {
+ handleConnectionDisconnected(wasError.booleanValue(), connection);
+ }
+
+ }));
+ signalConnections.add(connection.onConnectionFinished.connect(new Slot1<Boolean>() {
+
+ @Override
+ public void call(Boolean wasError) {
+ handleConnectFinished(wasError.booleanValue(), connection);
+ }
+
+ }));
+ signalConnections.add(connection.onSessionTerminated.connect(new Slot1<BOSHConnection.BOSHError>() {
+
+ @Override
+ public void call(BOSHError error) {
+ handleSessionTerminated(error);
+ }
+
+ }));
+ signalConnections.add(connection.onHTTPError.connect(new Slot1<String>() {
+
+ @Override
+ public void call(String httpErrorCode) {
+ handleHTTPError(httpErrorCode);
+ }
+
+ }));
+
+ if ("https".equals(boshURL_.getScheme())) {
+ boolean success = connection.setClientCertificate(clientCertificate_);
+ logger.fine("setClientCertificate, success: " + success + "\n");
+ }
+
+ connection.connect();
+ connections_.add(connection);
+ connectionsSignalConnections_.put(connection, signalConnections);
+ return connection;
+
+ }
+
+ private void destroyConnection(BOSHConnection connection) {
+ while (connections_.remove(connection)) {
+ // Loop will run till all instances of connection are removed
+ }
+ Set<SignalConnection> signalConnections = connectionsSignalConnections_.remove(connection);
+ if (signalConnections != null) {
+ for (SignalConnection signalConnection : signalConnections) {
+ if (signalConnection != null) {
+ signalConnection.disconnect();
+ }
+ }
+ signalConnections.clear();
+ }
+ }
+
+ private void tryToSendQueuedData() {
+ if (sid_.isEmpty()) {
+ // If we've not got as far as stream start yet, pend
+ return;
+ }
+
+ BOSHConnection suitableConnection = getSuitableConnection();
+ boolean toSend = !dataQueue_.isEmpty();
+ if (suitableConnection != null) {
+ if (toSend) {
+ rid_++;
+ suitableConnection.setRID(rid_);
+ SafeByteArray data = new SafeByteArray();
+ for(SafeByteArray datum : dataQueue_) {
+ data.append(datum);
+ }
+ suitableConnection.write(data);
+ dataQueue_.clear();
+ }
+ else if (pendingTerminate_) {
+ rid_++;
+ suitableConnection.setRID(rid_);
+ suitableConnection.terminateStream();
+ sid_ = "";
+ close();
+ }
+ }
+ if (!pendingTerminate_) {
+ // Ensure there's always a session waiting to read data for us
+ boolean pending = false;
+ for(BOSHConnection connection : connections_) {
+ if (connection != null && !connection.isReadyToSend()) {
+ pending = true;
+ }
+ }
+ if (!pending) {
+ if (restartCount_ >= 1) {
+ // Don't open a second connection until we've restarted the stream twice - i.e. we've authed and resource bound
+ if (suitableConnection != null) {
+ rid_++;
+ suitableConnection.setRID(rid_);
+ suitableConnection.write(new SafeByteArray());
+ }
+ else {
+ // My thought process I went through when writing this, to aid anyone else confused why this can happen...
+ //
+ // What to do here? I think this isn't possible.
+ // If you didn't have two connections, suitable would have made one.
+ // If you have two connections and neither is suitable, pending would be true.
+ // If you have a non-pending connection, it's suitable.
+ //
+ // If I decide to do something here, remove assert above.
+ //
+ // Ah! Yes, because there's a period between creating the connection and it being connected. */
+ }
+ }
+ }
+ }
+ }
+
+ private BOSHConnection getSuitableConnection() {
+ BOSHConnection suitableConnection = null;
+ for(BOSHConnection connection : connections_) {
+ if (connection.isReadyToSend()) {
+ suitableConnection = connection;
+ break;
+ }
+ }
+
+ if (suitableConnection == null && (connections_.size() < requestLimit_)) {
+ // This is not a suitable connection because it won't have yet connected and added TLS if needed.
+ BOSHConnection newConnection = createConnection();
+ newConnection.setSID(sid_);
+ }
+ assert(connections_.size() <= requestLimit_);
+ assert((suitableConnection == null) || suitableConnection.isReadyToSend());
+ return suitableConnection;
+ }
+
+}
diff --git a/src/com/isode/stroke/parser/BOSHBodyExtractor.java b/src/com/isode/stroke/parser/BOSHBodyExtractor.java
new file mode 100644
index 0000000..6b637b1
--- /dev/null
+++ b/src/com/isode/stroke/parser/BOSHBodyExtractor.java
@@ -0,0 +1,174 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.parser;
+
+import java.util.Arrays;
+
+import com.isode.stroke.base.ByteArray;
+
+public class BOSHBodyExtractor {
+
+ private BOSHBody body = null;
+
+ public BOSHBodyExtractor(XMLParserFactory parserFactory,ByteArray data) {
+ // Look for the opening body element
+ byte[] rawData = data.getData();
+ int i = 0;
+ while (i < rawData.length && isWhitespace((char) rawData[i])) {
+ ++i;
+ }
+ if ((rawData.length - i) < 6 || rawData[i] != '<' || rawData[i+1] != 'b'
+ || rawData[i+2] != 'o' || rawData[i+3] != 'd' || rawData[i+4] != 'y'
+ || !isEndCharacter((char) rawData[i+5])) {
+ return;
+ }
+ i += 5;
+
+
+ // Parse until end of element
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ boolean endStartTagSeen = false;
+ boolean endElementSeen = false;
+
+ for (; i < rawData.length; ++i) {
+ char c = (char) rawData[i];
+ if (inSingleQuote) {
+ if (c == '\'') {
+ inSingleQuote = false;
+ }
+ }
+ else if (inDoubleQuote) {
+ if (c == '"') {
+ inDoubleQuote = false;
+ }
+ }
+ else if (c == '\'') {
+ inSingleQuote = true;
+ }
+ else if (c == '"') {
+ inDoubleQuote = true;
+ }
+ else if (c == '/') {
+ if (i + 1 == rawData.length || rawData[i+1] != '>') {
+ return;
+ }
+ else {
+ endElementSeen = true;
+ endStartTagSeen = true;
+ i += 2;
+ break;
+ }
+ }
+ else if (c == '>') {
+ endStartTagSeen = true;
+ i += 1;
+ break;
+ }
+ }
+
+ if (!endStartTagSeen) {
+ return;
+ }
+
+ // Look for the end of the element
+
+ int j = rawData.length - 1;
+ if (!endElementSeen) {
+ while (isWhitespace((char) rawData[j]) && j > -1) {
+ j--;
+ }
+
+ if (j == -1 || rawData[j] != '>') {
+ return;
+ }
+ j--;
+
+ while (j > -1 && isWhitespace((char) rawData[j])) {
+ j--;
+ }
+
+ if (j < 6 || rawData[j-5] != '<' || rawData[j-4] != '/' || rawData[j-3] != 'b'
+ || rawData[j-2] != 'o' || rawData[j-1] != 'd' || rawData[j] != 'y' ) {
+ return;
+ }
+
+ j -= 6;
+ }
+
+ body = new BOSHBody();
+
+ if (!endElementSeen) {
+ byte[] rawBodyContent =
+ Arrays.copyOfRange(rawData, i, j+1);
+ body.content = (new ByteArray(rawBodyContent)).toString();
+ }
+
+ BOSHBodyParserClient parserClient = new BOSHBodyParserClient(this);
+ XMLParser parser = parserFactory.createParser(parserClient);
+ String stringToParse = data.toString().substring(0, i);
+ if(!parser.parse(stringToParse)) {
+ body = null;
+ }
+ }
+
+ public static boolean isWhitespace(char c) {
+ return c == ' ' || c == '\n' || c == '\t' || c == '\r';
+ }
+
+ private static boolean isEndCharacter(char c) {
+ return isWhitespace(c) || c == '>' || c == '/';
+ }
+
+ public BOSHBody getBody() {
+ return body;
+ }
+
+ public static class BOSHBody {
+ private AttributeMap attributes = new AttributeMap();
+ private String content = "";
+
+ public AttributeMap getAttributes() {
+ return attributes;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ }
+
+ private final static class BOSHBodyParserClient implements XMLParserClient {
+
+ private BOSHBodyParserClient(BOSHBodyExtractor bodyExtractor) {
+ bodyExtractor_ = bodyExtractor;
+ }
+
+ @Override
+ public void handleStartElement(String element, String ns,
+ AttributeMap attributes) {
+ bodyExtractor_.body.attributes = attributes;
+ }
+
+ @Override
+ public void handleEndElement(String element, String ns) {
+ // Empty Method
+ }
+
+ @Override
+ public void handleCharacterData(String data) {
+ // Empty Method
+ }
+
+ private final BOSHBodyExtractor bodyExtractor_;
+
+ }
+
+}
diff --git a/src/com/isode/stroke/parser/PlatformXMLParserFactory.java b/src/com/isode/stroke/parser/PlatformXMLParserFactory.java
index 132dbe7..68281b1 100644
--- a/src/com/isode/stroke/parser/PlatformXMLParserFactory.java
+++ b/src/com/isode/stroke/parser/PlatformXMLParserFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2012, Isode Limited, London, England.
+ * Copyright (c) 2010-2016, Isode Limited, London, England.
* All rights reserved.
*/
/*
@@ -10,9 +10,14 @@
package com.isode.stroke.parser;
-public class PlatformXMLParserFactory {
+public class PlatformXMLParserFactory extends XMLParserFactory {
public static XMLParser createXMLParser(XMLParserClient client) {
return new AaltoXMLParser(client);
}
+
+ @Override
+ public XMLParser createParser(XMLParserClient xmlParserClient) {
+ return createXMLParser(xmlParserClient);
+ }
}
diff --git a/src/com/isode/stroke/streamstack/DummyStreamLayer.java b/src/com/isode/stroke/streamstack/DummyStreamLayer.java
new file mode 100644
index 0000000..7f3dd15
--- /dev/null
+++ b/src/com/isode/stroke/streamstack/DummyStreamLayer.java
@@ -0,0 +1,51 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.streamstack;
+
+import com.isode.stroke.base.SafeByteArray;
+
+/**
+ * The {@link DummyStreamLayer} can be used to use a {@link LowLayer} on its own,
+ * without a functioning parent layer. The {@link DummyStreamLayer} will serve as the
+ * parent layer to the {@link LowLayer} and is called when the {@link LowLayer} wants
+ * to write data to its parent layer.
+ */
+public class DummyStreamLayer implements HighLayer {
+
+ private LowLayer childLayer;
+
+ public DummyStreamLayer(LowLayer lowLayer) {
+ childLayer = lowLayer;
+ childLayer.setParentLayer(this);
+ }
+
+ @Override
+ public void handleDataRead(SafeByteArray data) {
+ // Empty Method
+ }
+
+ @Override
+ public LowLayer getChildLayer() {
+ return childLayer;
+ }
+
+ @Override
+ public void setChildLayer(LowLayer childLayer) {
+ this.childLayer = childLayer;
+ }
+
+ @Override
+ public void writeDataToChildLayer(SafeByteArray data) {
+ if (childLayer != null) {
+ childLayer.writeData(data);
+ }
+ }
+
+}
diff --git a/test/com/isode/stroke/network/BOSHConnectionPoolTest.java b/test/com/isode/stroke/network/BOSHConnectionPoolTest.java
new file mode 100644
index 0000000..27b6928
--- /dev/null
+++ b/test/com/isode/stroke/network/BOSHConnectionPoolTest.java
@@ -0,0 +1,509 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.network;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.base.SafeByteArray;
+import com.isode.stroke.base.SafeString;
+import com.isode.stroke.base.URL;
+import com.isode.stroke.eventloop.DummyEventLoop;
+import com.isode.stroke.eventloop.Event.Callback;
+import com.isode.stroke.eventloop.EventLoop;
+import com.isode.stroke.network.BOSHConnection.BOSHError;
+import com.isode.stroke.parser.PlatformXMLParserFactory;
+import com.isode.stroke.signals.Slot;
+import com.isode.stroke.signals.Slot1;
+import com.isode.stroke.tls.TLSOptions;
+
+/**
+ * Tests for {@link BOSHConnectionPool}
+ */
+public class BOSHConnectionPoolTest {
+
+ private DummyEventLoop eventLoop = new DummyEventLoop();
+ private MockConnectionFactory connectionFactory = new MockConnectionFactory(eventLoop);
+ private List<String> xmppDataRead = new ArrayList<String>();
+ private List<String> boshDataRead = new ArrayList<String>();
+ private List<String> boshDataWritten = new ArrayList<String>();
+ private PlatformXMLParserFactory parserFactory = new PlatformXMLParserFactory();
+ private StaticDomainNameResolver resolver = new StaticDomainNameResolver(eventLoop);
+ private TimerFactory timerFactory = new DummyTimerFactory();
+ private String to = "wonderland.lit";
+ private String path = "/http-bind";
+ private String port = "5280";
+ private String sid = "MyShinySID";
+ private String initial = "<body wait='60' "
+ +"inactivity='30' "
+ +"polling='5' "
+ +"requests='2' "
+ +"hold='1' "
+ +"maxpause='120' "
+ +"sid='" + sid + "' "
+ +"ver='1.6' "
+ +"from='wonderland.lit' "
+ +"xmlns='http://jabber.org/protocol/httpbind'/>";
+ private URL boshURL = new URL("http", to, 5280, path);
+ private long initialRID = 2349876;
+ private int sessionStarted = 0;
+ private int sessionTerminated = 0;
+
+ @Before
+ public void setUp() {
+ resolver.addAddress(to, new HostAddress("127.0.0.1"));
+ }
+
+ @Test
+ public void testConnectionCount_OneWrite() {
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ eventLoop.processEvents();
+ assertEquals(0,sessionStarted);
+ readResponse(initial, connectionFactory.connections.get(0));
+ assertEquals(1,sessionStarted);
+ assertEquals(1,connectionFactory.connections.size());
+ testling.write(new SafeByteArray("<blah/>"));
+ eventLoop.processEvents();
+ assertEquals(1,connectionFactory.connections.size());
+ assertEquals(1,sessionStarted);
+ }
+
+ @Test
+ public void testConnectionCount_TwoWrites() {
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ eventLoop.processEvents();
+ readResponse(initial, connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ testling.write(new SafeByteArray("<blah/>"));
+ eventLoop.processEvents();
+ assertEquals(1,connectionFactory.connections.size());
+ testling.write(new SafeByteArray("<bleh/>"));
+ eventLoop.processEvents();
+ eventLoop.processEvents();
+ assertEquals(2,connectionFactory.connections.size());
+ }
+
+ @Test
+ public void testConnectionCount_ThreeWrites() {
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ eventLoop.processEvents();
+ readResponse(initial,connectionFactory.connections.get(0));
+ testling.restartStream();
+ readResponse("<body/>",connectionFactory.connections.get(0));
+ testling.restartStream();
+ readResponse("<body/>",connectionFactory.connections.get(0));
+ testling.write(new SafeByteArray("<blah/>"));
+ testling.write(new SafeByteArray("<bleh/>"));
+ testling.write(new SafeByteArray("<bluh/>"));
+ eventLoop.processEvents();
+ assertTrue("2 < "+connectionFactory.connections.size(),
+ 2 >= connectionFactory.connections.size());
+ }
+
+ @Test
+ public void testConnectionCount_ThreeWrites_ManualConnect() {
+ connectionFactory.autoFinishConnect = false;
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ assertEquals(0,boshDataWritten.size()); // Connection not connected yet, can't send data
+
+ connectionFactory.connections.get(0).onConnectFinished.emit(false);
+ eventLoop.processEvents();
+ assertEquals(1,boshDataWritten.size());
+
+ readResponse(initial, connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ assertEquals(1,connectionFactory.connections.size());
+ assertEquals(1,boshDataWritten.size()); // Don't respond to initial data with a holding call
+
+ testling.restartStream();;
+ eventLoop.processEvents();
+ readResponse("<body/>", connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ testling.restartStream();
+ eventLoop.processEvents();
+
+ testling.write(new SafeByteArray("<blah/>"));
+ eventLoop.processEvents();
+ assertEquals(2,connectionFactory.connections.size());
+ assertEquals(3,boshDataWritten.size()); // New connection isn't up yet
+
+ connectionFactory.connections.get(1).onConnectFinished.emit(false);
+ eventLoop.processEvents();
+ assertEquals(4,boshDataWritten.size()); // New Connection ready
+
+ testling.write(new SafeByteArray("<bleh/>"));
+ eventLoop.processEvents();
+ testling.write(new SafeByteArray("<bluh/>"));
+ assertEquals(4,boshDataWritten.size()); // New data can't be sent, no free connections
+ eventLoop.processEvents();
+ assertTrue("2 < "+connectionFactory.connections.size(),
+ 2 >= connectionFactory.connections.size());
+ }
+
+ @Test
+ public void testConnectionCount_ThreeWritesTwoReads() {
+ MockConnection c0 = null;
+ MockConnection c1 = null;
+ long rid = initialRID;
+
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ c0 = connectionFactory.connections.get(0);
+ eventLoop.processEvents();
+ assertEquals(1,boshDataWritten.size()); // header
+
+ rid++;
+ readResponse(initial, c0);
+ assertEquals(1,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+ assertFalse(c0.pending);
+
+ rid++;
+ testling.restartStream();
+ eventLoop.processEvents();
+ readResponse("<body/>", connectionFactory.connections.get(0));
+
+ rid++;
+ testling.write(new SafeByteArray("<blah/>"));
+ eventLoop.processEvents();
+ assertEquals(2,connectionFactory.connections.size()); // 0 was waiting for response, open and send on 1
+ assertEquals(4,boshDataWritten.size()); // data
+ c1 = connectionFactory.connections.get(1);
+ String fullBody = "<body rid='" + rid + "' sid='" + sid +
+ "' xmlns='http://jabber.org/protocol/httpbind'><blah/></body>"; // Check empty write
+ assertEquals(fullBody,lastBody());
+ assertTrue(c0.pending);
+ assertTrue(c1.pending);
+
+ rid++;
+ readResponse("<body xmlns='http://jabber.org/protocol/httpbind'><message><splatploing/></message></body>", c0); // Doesn't include necessary attributes - as the support is improved this'll start to fail
+ eventLoop.processEvents();
+ assertFalse(c0.pending);
+ assertTrue(c1.pending);
+ assertEquals(4,boshDataWritten.size()); // don't send empty in [0], still have [1] waiting
+ assertEquals(2,connectionFactory.connections.size());
+
+ rid++;
+ readResponse("<body xmlns='http://jabber.org/protocol/httpbind'><message><splatploing><blittlebarg/></splatploing></message></body>", c1);
+ eventLoop.processEvents();
+ assertFalse(c1.pending);
+ assertTrue(c0.pending);
+ assertEquals(5,boshDataWritten.size()); // Empty to make room
+ assertEquals(2,connectionFactory.connections.size());
+
+ rid++;
+ testling.write(new SafeByteArray("<bleh/>"));
+ eventLoop.processEvents();
+ assertTrue(c0.pending);
+ assertTrue(c1.pending);
+ assertEquals(6,boshDataWritten.size());
+
+ rid++;
+ testling.write(new SafeByteArray("<blush/>"));
+ assertTrue(c0.pending);
+ assertTrue(c1.pending);
+ assertEquals(6,boshDataWritten.size()); //Don't send data, no room
+ eventLoop.processEvents();
+ assertEquals(2,connectionFactory.connections.size());
+ }
+
+ @Test
+ public void testSession() {
+ to = "prosody.doomsong.co.uk";
+ resolver.addAddress("prosody.doomsong.co.uk",new HostAddress("127.0.0.1"));
+ path = "/http-bind/";
+ boshURL = new URL("http", to, 5280, path);
+
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ eventLoop.processEvents();
+ assertEquals(1,boshDataWritten.size()); // header
+ assertEquals(1,connectionFactory.connections.size());
+
+ String response = "<body authid='743da605-4c2e-4de1-afac-ac040dd4a940' xmpp:version='1.0' xmlns:stream='http://etherx.jabber.org/streams' xmlns:xmpp='urn:xmpp:xbosh' inactivity='60' wait='60' polling='5' secure='true' hold='1' from='prosody.doomsong.co.uk' ver='1.6' sid='743da605-4c2e-4de1-afac-ac040dd4a940' requests='2' xmlns='http://jabber.org/protocol/httpbind'><stream:features><auth xmlns='http://jabber.org/features/iq-auth'/><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>SCRAM-SHA-1</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms></stream:features></body>";
+ readResponse(response, connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ assertEquals(1,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+
+ String send = "<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"SCRAM-SHA-1\">biwsbj1hZG1pbixyPWZhOWE5ZDhiLWZmMDctNGE4Yy04N2E3LTg4YWRiNDQxZGUwYg==</auth>";
+ testling.write(new SafeByteArray(send));
+ eventLoop.processEvents();
+ assertEquals(2,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+
+ response = "<body xmlns='http://jabber.org/protocol/httpbind' sid='743da605-4c2e-4de1-afac-ac040dd4a940' xmlns:stream = 'http://etherx.jabber.org/streams'><challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cj1mYTlhOWQ4Yi1mZjA3LTRhOGMtODdhNy04OGFkYjQ0MWRlMGJhZmZlMWNhMy1mMDJkLTQ5NzEtYjkyNS0yM2NlNWQ2MDQyMjYscz1OVGd5WkdWaFptTXRaVE15WXkwMFpXUmhMV0ZqTURRdFpqYzRNbUppWmpGa1pqWXgsaT00MDk2</challenge></body>";
+ readResponse(response, connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ assertEquals(2,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+
+ send = "<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">Yz1iaXdzLHI9ZmE5YTlkOGItZmYwNy00YThjLTg3YTctODhhZGI0NDFkZTBiYWZmZTFjYTMtZjAyZC00OTcxLWI5MjUtMjNjZTVkNjA0MjI2LHA9aU11NWt3dDN2VWplU2RqL01Jb3VIRldkZjBnPQ==</response>";
+ testling.write(new SafeByteArray(send));
+ eventLoop.processEvents();
+ assertEquals(3,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+
+ response = "<body xmlns='http://jabber.org/protocol/httpbind' sid='743da605-4c2e-4de1-afac-ac040dd4a940' xmlns:stream = 'http://etherx.jabber.org/streams'><success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>dj1YNmNBY3BBOWxHNjNOOXF2bVQ5S0FacERrVm89</success></body>";
+ readResponse(response, connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ assertEquals(3,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+
+ testling.restartStream();
+ eventLoop.processEvents();
+ assertEquals(4,boshDataWritten.size());
+ assertEquals(1,connectionFactory.connections.size());
+
+ response = "<body xmpp:version='1.0' xmlns:stream='http://etherx.jabber.org/streams' xmlns:xmpp='urn:xmpp:xbosh' inactivity='60' wait='60' polling='5' secure='true' hold='1' from='prosody.doomsong.co.uk' ver='1.6' sid='743da605-4c2e-4de1-afac-ac040dd4a940' requests='2' xmlns='http://jabber.org/protocol/httpbind'><stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><required/></bind><session xmlns='urn:ietf:params:xml:ns:xmpp-session'><optional/></session><sm xmlns='urn:xmpp:sm:2'><optional/></sm></stream:features></body>";
+ readResponse(response, connectionFactory.connections.get(0));
+ eventLoop.processEvents();
+ assertEquals(5,boshDataWritten.size()); // Now we've authed (restarted) we should be keeping one query in flight so the server can reply to us at any time it wants.
+ assertEquals(1,connectionFactory.connections.size());
+
+ send = "<body rid='2821988967416214' sid='cf663f6b94279d4f' xmlns='http://jabber.org/protocol/httpbind'><iq id='session-bind' type='set'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>d5a9744036cd20a0</resource></bind></iq></body>";
+ testling.write(new SafeByteArray(send));
+ eventLoop.processEvents();
+ assertEquals(6,boshDataWritten.size());
+ assertEquals(2,connectionFactory.connections.size());
+ }
+
+ @Test
+ public void testWrite_Empty() {
+ MockConnection c0 = null;
+
+ BOSHConnectionPool testling = createTestling();
+ assertEquals(1,connectionFactory.connections.size());
+ c0 = connectionFactory.connections.get(0);
+
+ readResponse(initial, c0);
+ eventLoop.processEvents();
+ assertEquals(1,boshDataWritten.size()); // Shouldn't have sent anything extra
+ eventLoop.processEvents();
+ testling.restartStream();
+ eventLoop.processEvents();
+ assertEquals(2,boshDataWritten.size());
+ readResponse("<body></body>",c0);
+ eventLoop.processEvents();
+ assertEquals(3,boshDataWritten.size());
+ String fullBody = "<body rid='" + (initialRID + 2) + "' sid='" + sid + "' xmlns='http://jabber.org/protocol/httpbind'></body>";
+ String response = boshDataWritten.get(2);
+ int bodyPosition = response.indexOf("\r\n\r\n");
+ assertEquals(fullBody,response.substring(bodyPosition+4));
+ }
+
+
+ private static class MockConnection extends Connection {
+
+ private final EventLoop eventLoop;
+ private HostAddressPort hostAddressPort;
+ private final List<HostAddressPort> failingPorts;
+ private final ByteArray dataWritten = new ByteArray();
+ private boolean disconnected;
+ private boolean pending;
+ private boolean autoFinishConnect;
+
+ private MockConnection(Collection<? extends HostAddressPort> failingPorts,
+ EventLoop eventLoop,boolean autoFinishConnect) {
+ this.eventLoop = eventLoop;
+ this.failingPorts = new ArrayList<HostAddressPort>(failingPorts);
+ disconnected = false;
+ pending = false;
+ this.autoFinishConnect = autoFinishConnect;
+ }
+
+ @Override
+ public void listen() {
+ fail();
+ }
+
+ @Override
+ public void connect(HostAddressPort address) {
+ hostAddressPort = address;
+ final boolean fail = failingPorts.contains(address);
+ if (autoFinishConnect) {
+ eventLoop.postEvent(new Callback() {
+
+ @Override
+ public void run() {
+ onConnectFinished.emit(fail);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ disconnected = true;
+ onDisconnected.emit(null);
+ }
+
+ @Override
+ public void write(SafeByteArray data) {
+ dataWritten.append(data);
+ pending = true;
+ }
+
+ @Override
+ public HostAddressPort getLocalAddress() {
+ return new HostAddressPort();
+ }
+
+ public HostAddressPort getRemoteAddress() {
+ return new HostAddressPort();
+ }
+
+ }
+
+ private static class MockConnectionFactory implements ConnectionFactory {
+
+ private final EventLoop eventLoop;
+ private List<MockConnection> connections = new ArrayList<MockConnection>();
+ private List<HostAddressPort> failingPorts = new ArrayList<HostAddressPort>();
+ private boolean autoFinishConnect;
+
+ private MockConnectionFactory(EventLoop eventLoop) {
+ this(eventLoop,true);
+ }
+
+ private MockConnectionFactory(EventLoop eventLoop,boolean autoFinishConnect) {
+ this.eventLoop = eventLoop;
+ this.autoFinishConnect = autoFinishConnect;
+ }
+
+ @Override
+ public Connection createConnection() {
+ MockConnection connection =
+ new MockConnection(failingPorts, eventLoop, autoFinishConnect);
+ connections.add(connection);
+ return connection;
+ }
+
+ }
+
+ private BOSHConnectionPool createTestling() {
+ // make_shared is limited to 9 arguments; instead new is used here.
+ BOSHConnectionPool pool = new BOSHConnectionPool(boshURL, resolver, connectionFactory, parserFactory,
+ null, timerFactory, eventLoop, to, initialRID,
+ new URL(), new SafeString(""),
+ new SafeString(""), new TLSOptions());
+ pool.open();
+ pool.onXMPPDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleXMPPDataRead(data);
+ }
+
+ });
+ pool.onBOSHDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleBOSHDataRead(data);
+ }
+ });
+ pool.onBOSHDataWritten.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray data) {
+ handleBOSHDataWritten(data);
+ }
+
+
+ });
+ pool.onSessionStarted.connect(new Slot() {
+
+ @Override
+ public void call() {
+ handleSessionStarted();
+ }
+
+ });
+ pool.onSessionTerminated.connect(new Slot1<BOSHConnection.BOSHError>() {
+
+ @Override
+ public void call(BOSHError error) {
+ handleSessionTerminated();
+ }
+
+ });
+ eventLoop.processEvents();
+ eventLoop.processEvents();
+ return pool;
+ }
+
+ private String lastBody() {
+ String response = boshDataWritten.get(boshDataWritten.size() - 1);
+ int bodyPosition = response.indexOf("\r\n\r\n");
+ return response.substring(bodyPosition+4);
+ }
+
+ private void handleXMPPDataRead(SafeByteArray d) {
+ xmppDataRead.add(d.toString());
+ }
+
+ private void handleBOSHDataRead(SafeByteArray d) {
+ boshDataRead.add(d.toString());
+ }
+
+ private void handleBOSHDataWritten(SafeByteArray d) {
+ boshDataWritten.add(d.toString());
+ }
+
+ private void handleSessionStarted() {
+ sessionStarted++;
+ }
+
+ private void handleSessionTerminated() {
+ sessionTerminated++;
+ }
+
+ private void readResponse(String response, MockConnection connection) {
+ connection.pending = false;
+ SafeByteArray data1 = new SafeByteArray(
+ "HTTP/1.1 200 OK\r\n"
+ +"Content-Type: text/xml; charset=utf-8\r\n"
+ +"Access-Control-Allow-Origin: *\r\n"
+ +"Access-Control-Allow-Headers: Content-Type\r\n"
+ +"Content-Length: ");
+ connection.onDataRead.emit(data1);
+ SafeByteArray data2 = new SafeByteArray(String.valueOf(response.length()));
+ connection.onDataRead.emit(data2);
+ SafeByteArray data3 = new SafeByteArray("\r\n\r\n");
+ connection.onDataRead.emit(data3);
+ SafeByteArray data4 = new SafeByteArray(response);
+ connection.onDataRead.emit(data4);
+ }
+
+ private String fullRequestFor(String data) {
+ String result = "POST /" + path + " HTTP/1.1\r\n"
+ + "Host: " + to + ":" + port + "\r\n"
+ + "Content-Type: text/xml; charset=utf-8\r\n"
+ + "Content-Length: " + data.length() + "\r\n\r\n"
+ + data;
+ return result;
+ }
+
+}
diff --git a/test/com/isode/stroke/network/BoshConnectionTest.java b/test/com/isode/stroke/network/BoshConnectionTest.java
new file mode 100644
index 0000000..0fa4020
--- /dev/null
+++ b/test/com/isode/stroke/network/BoshConnectionTest.java
@@ -0,0 +1,352 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.network;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Test;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.base.SafeByteArray;
+import com.isode.stroke.base.URL;
+import com.isode.stroke.eventloop.DummyEventLoop;
+import com.isode.stroke.eventloop.Event.Callback;
+import com.isode.stroke.eventloop.EventLoop;
+import com.isode.stroke.network.BOSHConnection.Pair;
+import com.isode.stroke.parser.PlatformXMLParserFactory;
+import com.isode.stroke.signals.Slot1;
+import com.isode.stroke.signals.Slot2;
+import com.isode.stroke.tls.TLSContextFactory;
+import com.isode.stroke.tls.TLSOptions;
+
+/**
+ * Test for {@link BoshConnection}
+ */
+public class BoshConnectionTest {
+
+ private final DummyEventLoop eventLoop = new DummyEventLoop();
+ private final MockConnectionFactory connectionFactory = new MockConnectionFactory(eventLoop);
+ private boolean connectFinished = false;
+ private boolean connectFinishedWithError = false;
+ private boolean disconnected = false;
+ private boolean disconnectedError = false;
+ private final ByteArray dataRead = new ByteArray();
+ private final PlatformXMLParserFactory parserFactory = new PlatformXMLParserFactory();
+ private final StaticDomainNameResolver resolver = new StaticDomainNameResolver(eventLoop);
+ private final TimerFactory timerFactory = new DummyTimerFactory();
+ private final TLSContextFactory tlsContextFactory = null;
+ private String sid;
+
+ @After
+ public void tearDown() {
+ eventLoop.processEvents();
+ }
+
+ @Test
+ public void testHeader() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ testling.startStream("wonderland.lit",1);
+ String initial = "<body wait='60' "
+ +"inactivity='30' "
+ +"polling='5' "
+ +"requests='2' "
+ +"hold='1' "
+ +"maxpause='120' "
+ +"sid='MyShinySID' "
+ +"ver='1.6' "
+ +"from='wonderland.lit' "
+ +"xmlns='http://jabber.org/protocol/httpbind'/>";
+ readResponse(initial, connectionFactory.connections.get(0));
+ assertEquals("MyShinySID",sid);
+ assertTrue(testling.isReadyToSend());
+ }
+
+ @Test
+ public void testReadiness_ok() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ testling.setSID("blahhhh");
+ assertTrue(testling.isReadyToSend());
+ }
+
+ @Test
+ public void testReadiness_pending() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ testling.setSID("mySID");
+ assertTrue(testling.isReadyToSend());
+ testling.write(new SafeByteArray("<mypayload/>"));
+ assertFalse(testling.isReadyToSend());
+ readResponse("<body><blah/></body>", connectionFactory.connections.get(0));
+ assertTrue(testling.isReadyToSend());
+ }
+
+ @Test
+ public void testReadiness_disconnect() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ testling.setSID("mySID");
+ assertTrue(testling.isReadyToSend());
+ connectionFactory.connections.get(0).onDisconnected.emit(null);
+ assertFalse(testling.isReadyToSend());
+ }
+
+ @Test
+ public void testReadiness_noSID() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ assertFalse(testling.isReadyToSend());
+ }
+
+ @Test
+ public void testWrite_Receive() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ testling.setSID("mySID");
+ testling.write(new SafeByteArray("<mypayload/>"));
+ readResponse("<body><blah/></body>", connectionFactory.connections.get(0));
+ assertEquals("<blah/>",dataRead.toString());
+ }
+
+ @Test
+ public void testWrite_ReceiveTwice() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ testling.setSID("mySID");
+ testling.write(new SafeByteArray("<mypayload/>"));
+ readResponse("<body><blah/></body>", connectionFactory.connections.get(0));
+ assertEquals("<blah/>",dataRead.toString());
+ dataRead.clear();
+ testling.write(new SafeByteArray("<mypayload2/>"));
+ readResponse("<body><bleh/></body>", connectionFactory.connections.get(0));
+ assertEquals("<bleh/>",dataRead.toString());
+ }
+
+ @Test
+ public void testRead_Fragment() {
+ BOSHConnection testling = createTestling();
+ testling.connect();
+ eventLoop.processEvents();
+ assertEquals(1, connectionFactory.connections.size());
+ MockConnection connection = connectionFactory.connections.get(0);
+ SafeByteArray data1 = new SafeByteArray(
+ "HTTP/1.1 200 OK\r\n"+
+ "Content-Type: text/xml; charset=utf-8\r\n"+
+ "Access-Control-Allow-Origin: *\r\n"+
+ "Access-Control-Allow-Headers: Content-Type\r\n"+
+ "Content-Length: 64\r\n");
+ SafeByteArray data2 = new SafeByteArray(
+ "\r\n<body xmlns='http://jabber.org/protocol/httpbind'>"+
+ "<bl");
+ SafeByteArray data3 = new SafeByteArray(
+ "ah/>"+
+ "</body>");
+ connection.onDataRead.emit(data1);
+ connection.onDataRead.emit(data2);
+ assertTrue(dataRead.isEmpty());
+ connection.onDataRead.emit(data3);
+ assertEquals("<blah/>",dataRead.toString());
+ }
+
+ @Test
+ public void testHTTPRequest() {
+ String data = "<blah/>";
+ String sid = "wigglebloom";
+ String fullBody = "<body xmlns='http://jabber.org/protocol/httpbind' sid='" + sid + "' rid='20'>" + data + "</body>";
+ Pair<SafeByteArray, Integer> http =
+ BOSHConnection.createHTTPRequest(new SafeByteArray(data), false, false,
+ 20, sid, new URL());
+ assertEquals(fullBody.length(),http.second.intValue());
+ }
+
+ @Test
+ public void testHTTPRequest_Empty() {
+ String data = "";
+ String sid = "wigglebloomsickle";
+ String fullBody = "<body rid='42' sid='" + sid + "' xmlns='http://jabber.org/protocol/httpbind'>" + data + "</body>";
+ Pair<SafeByteArray, Integer> http =
+ BOSHConnection.createHTTPRequest(new SafeByteArray(data), false, false,
+ 42, sid, new URL());
+ assertEquals(fullBody.length(),http.second.intValue());
+ String response = http.first.toString();
+ int bodyPosition = response.indexOf("\r\n\r\n");
+ assertFalse("bodyPosition is equal to -1",-1 == bodyPosition);
+ assertEquals(fullBody,response.substring(bodyPosition+4));
+ }
+
+ private BOSHConnection createTestling() {
+ resolver.addAddress("wonderland.lit", new HostAddress("127.0.0.1"));
+ Connector connector = Connector.create("wonderland.lit", 5280, null, resolver, connectionFactory, timerFactory);
+ BOSHConnection connection = BOSHConnection.create(new URL(), connector, parserFactory,
+ tlsContextFactory, new TLSOptions());
+ connection.onConnectionFinished.connect(new Slot1<Boolean>() {
+
+ @Override
+ public void call(Boolean hadError) {
+ handleConnectFinished(hadError.booleanValue());
+ }
+
+ });
+ connection.onDisconnected.connect(new Slot1<Boolean>() {
+
+ @Override
+ public void call(Boolean hadError) {
+ handleDisconnected(hadError.booleanValue());
+ }
+
+ });
+ connection.onXMPPDataRead.connect(new Slot1<SafeByteArray>() {
+
+ @Override
+ public void call(SafeByteArray p1) {
+ handleDataRead(p1);
+ }
+
+ });
+ connection.onSessionStarted.connect(new Slot2<String, Integer>() {
+
+ @Override
+ public void call(String sid, Integer requests) {
+ handleSID(sid);
+ }
+
+ });
+ connection.setRID(42);
+ return connection;
+ }
+
+ private void handleConnectFinished(boolean hadError) {
+ connectFinished = true;
+ connectFinishedWithError = hadError;
+ }
+
+ private void handleDisconnected(boolean hadError) {
+ disconnected = true;
+ disconnectedError = hadError;
+ }
+
+ private void handleDataRead(SafeByteArray data) {
+ dataRead.append(data);
+ }
+
+ private void handleSID(String s) {
+ sid = s;
+ }
+
+ private void readResponse(String response,MockConnection connection) {
+ SafeByteArray data1 = new SafeByteArray(
+ "HTTP/1.1 200 OK\r\n"+
+ "Content-Type: text/xml; charset=utf-8\r\n"+
+ "Access-Control-Allow-Origin: *\r\n"+
+ "Access-Control-Allow-Headers: Content-Type\r\n"+
+ "Content-Length: "
+ );
+ connection.onDataRead.emit(data1);
+ SafeByteArray data2 = new SafeByteArray(Integer.toString(response.length()));
+ connection.onDataRead.emit(data2);
+ SafeByteArray data3 = new SafeByteArray("\r\n\r\n");
+ connection.onDataRead.emit(data3);
+ SafeByteArray data4 = new SafeByteArray(response);
+ connection.onDataRead.emit(data4);
+ }
+
+ private static class MockConnection extends Connection {
+
+ public MockConnection(Collection<HostAddressPort> failingPorts,
+ EventLoop eventLoop) {
+ this.failingPorts = new ArrayList<HostAddressPort>(failingPorts);
+ this.eventLoop = eventLoop;
+ }
+
+ @Override
+ public void listen() {
+ fail();
+ }
+
+ @Override
+ public void connect(HostAddressPort address) {
+ hostAddressPort = address;
+ final boolean fail = failingPorts.contains(address);
+ eventLoop.postEvent(new Callback() {
+
+ @Override
+ public void run() {
+ onConnectFinished.emit(fail);
+ }
+
+ });
+ }
+
+ @Override
+ public void disconnect() {
+ disconnected = true;
+ onDisconnected.emit(null);
+ }
+
+ @Override
+ public void write(SafeByteArray data) {
+ dataWritten.append(data);
+ }
+
+ /* (non-Javadoc)
+ * @see com.isode.stroke.network.Connection#getLocalAddress()
+ */
+ @Override
+ public HostAddressPort getLocalAddress() {
+ return new HostAddressPort();
+ }
+
+ public HostAddressPort getRemoteAddress() {
+ return new HostAddressPort();
+ }
+
+ private final EventLoop eventLoop;
+ private HostAddressPort hostAddressPort;
+ private final List<HostAddressPort> failingPorts;
+ private final ByteArray dataWritten = new ByteArray();
+ private boolean disconnected;
+
+ }
+
+ private static class MockConnectionFactory implements ConnectionFactory {
+
+ public MockConnectionFactory(EventLoop eventLoop) {
+ this.eventLoop = eventLoop;
+ }
+
+ @Override
+ public Connection createConnection() {
+ MockConnection connection = new MockConnection(failingPorts, eventLoop);
+ connections.add(connection);
+ return connection;
+ }
+
+ private final EventLoop eventLoop;
+ private List<MockConnection> connections = new ArrayList<MockConnection>();
+ private List<HostAddressPort> failingPorts = new ArrayList<HostAddressPort>();
+ }
+
+}
diff --git a/test/com/isode/stroke/parser/BOSHBodyExtractorTest.java b/test/com/isode/stroke/parser/BOSHBodyExtractorTest.java
new file mode 100644
index 0000000..e8f0db5
--- /dev/null
+++ b/test/com/isode/stroke/parser/BOSHBodyExtractorTest.java
@@ -0,0 +1,101 @@
+/* Copyright (c) 2016, Isode Limited, London, England.
+ * All rights reserved.
+ *
+ * Acquisition and use of this software and related materials for any
+ * purpose requires a written license agreement from Isode Limited,
+ * or a written license from an organisation licensed by Isode Limited
+ * to grant such a license.
+ *
+ */
+package com.isode.stroke.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.isode.stroke.base.ByteArray;
+
+/**
+ * Tests for {@link BOSHBodyExtractor}
+ */
+public class BOSHBodyExtractorTest {
+
+ private final PlatformXMLParserFactory parserFactory = new PlatformXMLParserFactory();
+
+ @Test
+ public void testGetBody() {
+ ByteArray data = new ByteArray("<body a1='a\"1' a2=\"a'2\" boo='bar' >"
+ +"foo <message> <body> bar"
+ +"</body > ");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNotNull(testling.getBody());
+ assertEquals("a\"1",testling.getBody().getAttributes().getAttribute("a1"));
+ assertEquals("foo <message> <body> bar",testling.getBody().getContent());
+ }
+
+ @Test
+ public void testGetBody_EmptyContent() {
+ ByteArray data = new ByteArray("<body foo='bar'/>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+
+ assertNotNull(testling.getBody());
+ assertEquals("bar",testling.getBody().getAttributes().getAttribute("foo"));
+ assertTrue(testling.getBody().getContent().isEmpty());
+ }
+
+ @Test
+ public void testGetBody_EmptyContent2() {
+ ByteArray data = new ByteArray("<body foo='bar'></body>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+
+ assertNotNull(testling.getBody());
+ assertEquals("bar",testling.getBody().getAttributes().getAttribute("foo"));
+ assertTrue(testling.getBody().getContent().isEmpty());
+ }
+
+ @Test
+ public void testGetBody_EmptyElementEmptyContent() {
+ ByteArray data = new ByteArray("<body/>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNotNull(testling.getBody());
+ }
+
+ @Test
+ public void testGetBody_InvalidStartTag() {
+ ByteArray data = new ByteArray("<bodi></body>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNull(testling.getBody());
+ }
+
+ @Test
+ public void testGetBody_InvalidStartTag2() {
+ ByteArray data = new ByteArray("<bodyy></body>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNull(testling.getBody());
+ }
+
+ @Test
+ public void testGetBody_IncompleteStartTag() {
+ ByteArray data = new ByteArray("<body");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNull(testling.getBody());
+ }
+
+ @Test
+ public void testGetBody_InvalidEndTag() {
+ ByteArray data = new ByteArray("<body></bodi>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNull(testling.getBody());
+ }
+
+ @Test
+ public void testGetBody_InvalidEndTag2() {
+ ByteArray data = new ByteArray("<body><b/body>");
+ BOSHBodyExtractor testling = new BOSHBodyExtractor(parserFactory, data);
+ assertNull(testling.getBody());
+ }
+
+}