1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-06-25 04:44:49 +02:00
Smack/smack-bosh/src/main/java/org/jivesoftware/smack/bosh/XMPPBOSHConnection.java
Florian Schmaus bfc14227ca Propagate stream errors on connect/login to the caller
Before this, if there was a stream error response by the server to our
stream open, that error response would only be handled in the reader
thread, and the user would get a message like:

"org.jivesoftware.smack.SmackException$NoResponseException: No
response received within reply timeout. Timeout was
5000ms (~5s). While waiting for SASL mechanisms stream feature from
server"

while the server may actually sent something like

<stream:stream
  xmlns='jabber:client'
  xmlns:stream='http://etherx.jabber.org/streams'
  id='6785787028201586334'
  from='jabbim.com'
  version='1.0'
  xml:lang='en'>
  <stream:error>
    <policy-violation xmlns='urn:ietf:params:xml:ns:xmpp-streams'>
	</policy-violation>
	<text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-streams'>
	  Too many (2) failed authentications from this IP
      address (1xx.66.xx.xxx). The address will be unblocked at 04:24:00
      06.01.2017 UTC
    </text>
  </stream:error>
</stream:stream>

It was necessary to change saslFeatureReceived from SmackException to
XMPPException in order to return the StreamErrorException at this sync
point. But this change in return required the introduction of a
tlsHandled sync point for SmackException (which just acts as a wrapper
for the various exception types that could occurn when establishing
TLS). The tlsHandled sync point is marked successful even if no TLS
was established in case none was required and/or if not supported by
the server.
2017-01-07 10:38:41 +01:00

532 lines
20 KiB
Java

/**
*
* Copyright 2009 Jive Software.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.bosh;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.io.StringReader;
import java.io.Writer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.ConnectionException;
import org.jivesoftware.smack.XMPPException.StreamErrorException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Element;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.Nonza;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.parts.Resourcepart;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import org.igniterealtime.jbosh.AbstractBody;
import org.igniterealtime.jbosh.BOSHClient;
import org.igniterealtime.jbosh.BOSHClientConfig;
import org.igniterealtime.jbosh.BOSHClientConnEvent;
import org.igniterealtime.jbosh.BOSHClientConnListener;
import org.igniterealtime.jbosh.BOSHClientRequestListener;
import org.igniterealtime.jbosh.BOSHClientResponseListener;
import org.igniterealtime.jbosh.BOSHException;
import org.igniterealtime.jbosh.BOSHMessageEvent;
import org.igniterealtime.jbosh.BodyQName;
import org.igniterealtime.jbosh.ComposableBody;
/**
* Creates a connection to an XMPP server via HTTP binding.
* This is specified in the XEP-0206: XMPP Over BOSH.
*
* @see XMPPConnection
* @author Guenther Niess
*/
public class XMPPBOSHConnection extends AbstractXMPPConnection {
private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName());
/**
* The XMPP Over Bosh namespace.
*/
public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
/**
* The BOSH namespace from XEP-0124.
*/
public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
/**
* The used BOSH client from the jbosh library.
*/
private BOSHClient client;
/**
* Holds the initial configuration used while creating the connection.
*/
private final BOSHConfiguration config;
// Some flags which provides some info about the current state.
private boolean isFirstInitialization = true;
private boolean done = false;
// The readerPipe and consumer thread are used for the debugger.
private PipedWriter readerPipe;
private Thread readerConsumer;
/**
* The session ID for the BOSH session with the connection manager.
*/
protected String sessionID = null;
private boolean notified;
/**
* Create a HTTP Binding connection to an XMPP server.
*
* @param username the username to use.
* @param password the password to use.
* @param https true if you want to use SSL
* (e.g. false for http://domain.lt:7070/http-bind).
* @param host the hostname or IP address of the connection manager
* (e.g. domain.lt for http://domain.lt:7070/http-bind).
* @param port the port of the connection manager
* (e.g. 7070 for http://domain.lt:7070/http-bind).
* @param filePath the file which is described by the URL
* (e.g. /http-bind for http://domain.lt:7070/http-bind).
* @param xmppServiceDomain the XMPP service name
* (e.g. domain.lt for the user alice@domain.lt)
*/
public XMPPBOSHConnection(String username, String password, boolean https, String host, int port, String filePath, DomainBareJid xmppServiceDomain) {
this(BOSHConfiguration.builder().setUseHttps(https).setHost(host)
.setPort(port).setFile(filePath).setXmppDomain(xmppServiceDomain)
.setUsernameAndPassword(username, password).build());
}
/**
* Create a HTTP Binding connection to an XMPP server.
*
* @param config The configuration which is used for this connection.
*/
public XMPPBOSHConnection(BOSHConfiguration config) {
super(config);
this.config = config;
}
@Override
protected void connectInternal() throws SmackException, InterruptedException {
done = false;
notified = false;
try {
// Ensure a clean starting state
if (client != null) {
client.close();
client = null;
}
sessionID = null;
// Initialize BOSH client
BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
.create(config.getURI(), config.getXMPPServiceDomain().toString());
if (config.isProxyEnabled()) {
cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
}
client = BOSHClient.create(cfgBuilder.build());
client.addBOSHClientConnListener(new BOSHConnectionListener());
client.addBOSHClientResponseListener(new BOSHPacketReader());
// Initialize the debugger
if (config.isDebuggerEnabled()) {
initDebugger();
if (isFirstInitialization) {
if (debugger.getReaderListener() != null) {
addAsyncStanzaListener(debugger.getReaderListener(), null);
}
if (debugger.getWriterListener() != null) {
addPacketSendingListener(debugger.getWriterListener(), null);
}
}
}
// Send the session creation request
client.send(ComposableBody.builder()
.setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
.setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
.build());
} catch (Exception e) {
throw new ConnectionException(e);
}
// Wait for the response from the server
synchronized (this) {
if (!connected) {
final long deadline = System.currentTimeMillis() + getPacketReplyTimeout();
while (!notified) {
final long now = System.currentTimeMillis();
if (now >= deadline) break;
wait(deadline - now);
}
}
}
// If there is no feedback, throw an remote server timeout error
if (!connected && !done) {
done = true;
String errorMessage = "Timeout reached for the connection to "
+ getHost() + ":" + getPort() + ".";
throw new SmackException(errorMessage);
}
tlsHandled.reportSuccess();
saslFeatureReceived.reportSuccess();
}
public boolean isSecureConnection() {
// TODO: Implement SSL usage
return false;
}
public boolean isUsingCompression() {
// TODO: Implement compression
return false;
}
@Override
protected void loginInternal(String username, String password, Resourcepart resource) throws XMPPException,
SmackException, IOException, InterruptedException {
// Authenticate using SASL
saslAuthentication.authenticate(username, password, config.getAuthzid(), null);
bindResourceAndEstablishSession(resource);
afterSuccessfulLogin(false);
}
@Override
public void sendNonza(Nonza element) throws NotConnectedException {
if (done) {
throw new NotConnectedException();
}
sendElement(element);
}
@Override
protected void sendStanzaInternal(Stanza packet) throws NotConnectedException {
sendElement(packet);
}
private void sendElement(Element element) {
try {
send(ComposableBody.builder().setPayloadXML(element.toXML().toString()).build());
if (element instanceof Stanza) {
firePacketSendingListeners((Stanza) element);
}
}
catch (BOSHException e) {
LOGGER.log(Level.SEVERE, "BOSHException in sendStanzaInternal", e);
}
}
/**
* Closes the connection by setting presence to unavailable and closing the
* HTTP client. The shutdown logic will be used during a planned disconnection or when
* dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
* BOSH stanza(/packet) reader will not be removed; thus connection's state is kept.
*
*/
@Override
protected void shutdown() {
setWasAuthenticated();
sessionID = null;
done = true;
authenticated = false;
connected = false;
isFirstInitialization = false;
// Close down the readers and writers.
if (readerPipe != null) {
try {
readerPipe.close();
}
catch (Throwable ignore) { /* ignore */ }
reader = null;
}
if (reader != null) {
try {
reader.close();
}
catch (Throwable ignore) { /* ignore */ }
reader = null;
}
if (writer != null) {
try {
writer.close();
}
catch (Throwable ignore) { /* ignore */ }
writer = null;
}
readerConsumer = null;
}
/**
* Send a HTTP request to the connection manager with the provided body element.
*
* @param body the body which will be sent.
*/
protected void send(ComposableBody body) throws BOSHException {
if (!connected) {
throw new IllegalStateException("Not connected to a server!");
}
if (body == null) {
throw new NullPointerException("Body mustn't be null!");
}
if (sessionID != null) {
body = body.rebuild().setAttribute(
BodyQName.create(BOSH_URI, "sid"), sessionID).build();
}
client.send(body);
}
/**
* Initialize the SmackDebugger which allows to log and debug XML traffic.
*/
protected void initDebugger() {
// TODO: Maybe we want to extend the SmackDebugger for simplification
// and a performance boost.
// Initialize a empty writer which discards all data.
writer = new Writer() {
public void write(char[] cbuf, int off, int len) { /* ignore */}
public void close() { /* ignore */ }
public void flush() { /* ignore */ }
};
// Initialize a pipe for received raw data.
try {
readerPipe = new PipedWriter();
reader = new PipedReader(readerPipe);
}
catch (IOException e) {
// Ignore
}
// Call the method from the parent class which initializes the debugger.
super.initDebugger();
// Add listeners for the received and sent raw data.
client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
public void responseReceived(BOSHMessageEvent event) {
if (event.getBody() != null) {
try {
readerPipe.write(event.getBody().toXML());
readerPipe.flush();
} catch (Exception e) {
// Ignore
}
}
}
});
client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
public void requestSent(BOSHMessageEvent event) {
if (event.getBody() != null) {
try {
writer.write(event.getBody().toXML());
} catch (Exception e) {
// Ignore
}
}
}
});
// Create and start a thread which discards all read data.
readerConsumer = new Thread() {
private Thread thread = this;
private int bufferLength = 1024;
public void run() {
try {
char[] cbuf = new char[bufferLength];
while (readerConsumer == thread && !done) {
reader.read(cbuf, 0, bufferLength);
}
} catch (IOException e) {
// Ignore
}
}
};
readerConsumer.setDaemon(true);
readerConsumer.start();
}
/**
* Sends out a notification that there was an error with the connection
* and closes the connection.
*
* @param e the exception that causes the connection close event.
*/
protected void notifyConnectionError(Exception e) {
// Closes the connection temporary. A reconnection is possible
shutdown();
callConnectionClosedOnErrorListener(e);
}
/**
* A listener class which listen for a successfully established connection
* and connection errors and notifies the BOSHConnection.
*
* @author Guenther Niess
*/
private class BOSHConnectionListener implements BOSHClientConnListener {
/**
* Notify the BOSHConnection about connection state changes.
* Process the connection listeners and try to login if the
* connection was formerly authenticated and is now reconnected.
*/
public void connectionEvent(BOSHClientConnEvent connEvent) {
try {
if (connEvent.isConnected()) {
connected = true;
if (isFirstInitialization) {
isFirstInitialization = false;
}
else {
if (wasAuthenticated) {
try {
login();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
notifyReconnection();
}
}
else {
if (connEvent.isError()) {
// TODO Check why jbosh's getCause returns Throwable here. This is very
// unusual and should be avoided if possible
Throwable cause = connEvent.getCause();
Exception e;
if (cause instanceof Exception) {
e = (Exception) cause;
} else {
e = new Exception(cause);
}
notifyConnectionError(e);
}
connected = false;
}
}
finally {
notified = true;
synchronized (XMPPBOSHConnection.this) {
XMPPBOSHConnection.this.notifyAll();
}
}
}
}
/**
* Listens for XML traffic from the BOSH connection manager and parses it into
* stanza(/packet) objects.
*
* @author Guenther Niess
*/
private class BOSHPacketReader implements BOSHClientResponseListener {
/**
* Parse the received packets and notify the corresponding connection.
*
* @param event the BOSH client response which includes the received packet.
*/
public void responseReceived(BOSHMessageEvent event) {
AbstractBody body = event.getBody();
if (body != null) {
try {
if (sessionID == null) {
sessionID = body.getAttribute(BodyQName.create(XMPPBOSHConnection.BOSH_URI, "sid"));
}
if (streamId == null) {
streamId = body.getAttribute(BodyQName.create(XMPPBOSHConnection.BOSH_URI, "authid"));
}
final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(new StringReader(body.toXML()));
int eventType = parser.getEventType();
do {
eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
switch (name) {
case Message.ELEMENT:
case IQ.IQ_ELEMENT:
case Presence.ELEMENT:
parseAndProcessStanza(parser);
break;
case "challenge":
// The server is challenging the SASL authentication
// made by the client
final String challengeData = parser.nextText();
getSASLAuthentication().challengeReceived(challengeData);
break;
case "success":
send(ComposableBody.builder().setNamespaceDefinition("xmpp",
XMPPBOSHConnection.XMPP_BOSH_NS).setAttribute(
BodyQName.createWithPrefix(XMPPBOSHConnection.XMPP_BOSH_NS, "restart",
"xmpp"), "true").setAttribute(
BodyQName.create(XMPPBOSHConnection.BOSH_URI, "to"), getXMPPServiceDomain().toString()).build());
Success success = new Success(parser.nextText());
getSASLAuthentication().authenticated(success);
break;
case "features":
parseFeatures(parser);
break;
case "failure":
if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) {
final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser);
getSASLAuthentication().authenticationFailed(failure);
}
break;
case "error":
throw new StreamErrorException(PacketParserUtils.parseStreamError(parser));
}
break;
}
}
while (eventType != XmlPullParser.END_DOCUMENT);
}
catch (Exception e) {
if (isConnected()) {
notifyConnectionError(e);
}
}
}
}
}
}