Smack/smack-bosh/src/main/java/org/jivesoftware/smack/XMPPBOSHConnection.java

616 lines
21 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;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.io.Writer;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.sasl.SaslException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.AlreadyLoggedInException;
import org.jivesoftware.smack.SmackException.ConnectionException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.util.StringUtils;
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 a XMPP server via HTTP binding.
* This is specified in the XEP-0206: XMPP Over BOSH.
*
* @see XMPPConnection
* @author Guenther Niess
*/
public class XMPPBOSHConnection extends XMPPConnection {
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 connected = false;
private boolean authenticated = false;
private boolean anonymous = false;
private boolean isFirstInitialization = true;
private boolean wasAuthenticated = false;
private boolean done = false;
// The readerPipe and consumer thread are used for the debugger.
private PipedWriter readerPipe;
private Thread readerConsumer;
/**
* The BOSH equivalent of the stream ID which is used for DIGEST authentication.
*/
protected String authID = null;
/**
* The session ID for the BOSH session with the connection manager.
*/
protected String sessionID = null;
/**
* The full JID of the authenticated user.
*/
private String user = null;
/**
* Create a HTTP Binding connection to a XMPP server.
*
* @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 xmppDomain the XMPP service name
* (e.g. domain.lt for the user alice@domain.lt)
*/
public XMPPBOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
this.config = (BOSHConfiguration) getConfiguration();
}
/**
* Create a HTTP Binding connection to a XMPP server.
*
* @param config The configuration which is used for this connection.
*/
public XMPPBOSHConnection(BOSHConfiguration config) {
super(config);
this.config = config;
}
@Override
void connectInternal() throws SmackException {
if (connected) {
throw new IllegalStateException("Already connected to a server.");
}
done = false;
try {
// Ensure a clean starting state
if (client != null) {
client.close();
client = null;
}
sessionID = null;
authID = null;
// Initialize BOSH client
BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
.create(config.getURI(), config.getServiceName());
if (config.isProxyEnabled()) {
cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
}
client = BOSHClient.create(cfgBuilder.build());
client.addBOSHClientConnListener(new BOSHConnectionListener(this));
client.addBOSHClientResponseListener(new BOSHPacketReader(this));
// Initialize the debugger
if (config.isDebuggerEnabled()) {
initDebugger();
if (isFirstInitialization) {
if (debugger.getReaderListener() != null) {
addPacketListener(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) {
try {
wait(getPacketReplyTimeout());
}
catch (InterruptedException e) {}
}
}
// 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);
}
callConnectionConnectedListener();
}
public String getConnectionID() {
if (!connected) {
return null;
} else if (authID != null) {
return authID;
} else {
return sessionID;
}
}
public String getUser() {
return user;
}
public boolean isAnonymous() {
return anonymous;
}
public boolean isAuthenticated() {
return authenticated;
}
public boolean isConnected() {
return connected;
}
public boolean isSecureConnection() {
// TODO: Implement SSL usage
return false;
}
public boolean isUsingCompression() {
// TODO: Implement compression
return false;
}
public void login(String username, String password, String resource)
throws XMPPException, SmackException, IOException {
if (!isConnected()) {
throw new NotConnectedException();
}
if (authenticated) {
throw new AlreadyLoggedInException();
}
// Do partial version of nameprep on the username.
username = username.toLowerCase(Locale.US).trim();
if (saslAuthentication.hasNonAnonymousAuthentication()) {
// Authenticate using SASL
if (password != null) {
saslAuthentication.authenticate(username, password, resource);
} else {
saslAuthentication.authenticate(resource, config.getCallbackHandler());
}
} else {
throw new SaslException("No non-anonymous SASL authentication mechanism available");
}
String response = bindResourceAndEstablishSession(resource);
// Set the user.
if (response != null) {
this.user = response;
// Update the serviceName with the one returned by the server
config.setServiceName(StringUtils.parseServer(response));
} else {
this.user = username + "@" + getServiceName();
if (resource != null) {
this.user += "/" + resource;
}
}
// Set presence to online.
if (config.isSendPresence()) {
sendPacket(new Presence(Presence.Type.available));
}
// Indicate that we're now authenticated.
authenticated = true;
anonymous = false;
// Stores the autentication for future reconnection
config.setLoginInfo(username, password, resource);
// If debugging is enabled, change the the debug window title to include
// the
// name we are now logged-in as.l
if (config.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
callConnectionAuthenticatedListener();
}
public void loginAnonymously() throws XMPPException, SmackException, IOException {
if (!isConnected()) {
throw new NotConnectedException();
}
if (authenticated) {
throw new AlreadyLoggedInException();
}
if (saslAuthentication.hasAnonymousAuthentication()) {
saslAuthentication.authenticateAnonymously();
}
else {
// Authenticate using Non-SASL
throw new SaslException("No anonymous SASL authentication mechanism available");
}
String response = bindResourceAndEstablishSession(null);
// Set the user value.
this.user = response;
// Update the serviceName with the one returned by the server
config.setServiceName(StringUtils.parseServer(response));
// Set presence to online.
if (config.isSendPresence()) {
sendPacket(new Presence(Presence.Type.available));
}
// Indicate that we're now authenticated.
authenticated = true;
anonymous = true;
// If debugging is enabled, change the the debug window title to include the
// name we are now logged-in as.
// If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
// will be null
if (config.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
callConnectionAuthenticatedListener();
}
void sendPacketInternal(Packet packet) {
if (!done) {
try {
send(ComposableBody.builder().setPayloadXML(packet.toXML().toString())
.build());
} catch (BOSHException e) {
LOGGER.log(Level.SEVERE, "BOSHException in sendPacketInternal", e);
}
}
}
public void disconnect(Presence unavailablePresence) {
if (!connected) {
return;
}
shutdown(unavailablePresence);
// Cleanup
// TODO still needed? Smack 4.0.0 BOSH
// if (roster != null) {
// roster.cleanup();
// roster = null;
// }
sendListeners.clear();
recvListeners.clear();
collectors.clear();
interceptors.clear();
// Reset the connection flags
wasAuthenticated = false;
isFirstInitialization = true;
callConnectionClosedListener();
}
/**
* 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 packet reader and {@link Roster} will not be removed; thus
* connection's state is kept.
*
* @param unavailablePresence the presence packet to send during shutdown.
*/
protected void shutdown(Presence unavailablePresence) {
setWasAuthenticated(authenticated);
authID = null;
sessionID = null;
done = true;
authenticated = false;
connected = false;
isFirstInitialization = false;
try {
client.disconnect(ComposableBody.builder()
.setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
.setPayloadXML(unavailablePresence.toXML().toString())
.build());
// Wait 150 ms for processes to clean-up, then shutdown.
Thread.sleep(150);
}
catch (Exception e) {
// Ignore.
}
// 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;
}
/**
* Sets whether the connection has already logged in the server.
*
* @param wasAuthenticated true if the connection has already been authenticated.
*/
private void setWasAuthenticated(boolean wasAuthenticated) {
if (!this.wasAuthenticated) {
this.wasAuthenticated = wasAuthenticated;
}
}
/**
* 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(new Presence(Presence.Type.unavailable));
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 {
private final XMPPBOSHConnection connection;
public BOSHConnectionListener(XMPPBOSHConnection connection) {
this.connection = connection;
}
/**
* 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;
for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
listener.connectionCreated(connection);
}
}
else {
try {
if (wasAuthenticated) {
connection.login(
config.getUsername(),
config.getPassword(),
config.getResource());
}
for (ConnectionListener listener : getConnectionListeners()) {
listener.reconnectionSuccessful();
}
}
catch (Exception e) {
for (ConnectionListener listener : getConnectionListeners()) {
listener.reconnectionFailed(e);
}
}
}
}
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 {
synchronized (connection) {
connection.notifyAll();
}
}
}
}
}