diff options
Diffstat (limited to 'src/com/isode/stroke/network/BOSHConnection.java')
-rw-r--r-- | src/com/isode/stroke/network/BOSHConnection.java | 527 |
1 files changed, 527 insertions, 0 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); + } + } + +} |