diff options
Diffstat (limited to 'src/com/isode')
-rw-r--r-- | src/com/isode/stroke/network/BOSHConnection.java | 527 | ||||
-rw-r--r-- | src/com/isode/stroke/network/BOSHConnectionPool.java | 417 | ||||
-rw-r--r-- | src/com/isode/stroke/parser/BOSHBodyExtractor.java | 174 | ||||
-rw-r--r-- | src/com/isode/stroke/parser/PlatformXMLParserFactory.java | 9 | ||||
-rw-r--r-- | src/com/isode/stroke/streamstack/DummyStreamLayer.java | 51 |
5 files changed, 1176 insertions, 2 deletions
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); + } + } + +} |