| /** |
| * $RCSfile$ |
| * $Revision$ |
| * $Date$ |
| * |
| * Copyright 2009 Jive Software. |
| * |
| * All rights reserved. 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.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ThreadFactory; |
| |
| import org.jivesoftware.smack.Connection; |
| import org.jivesoftware.smack.ConnectionCreationListener; |
| import org.jivesoftware.smack.ConnectionListener; |
| import org.jivesoftware.smack.PacketCollector; |
| 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.packet.XMPPError; |
| import org.jivesoftware.smack.util.StringUtils; |
| |
| import com.kenai.jbosh.BOSHClient; |
| import com.kenai.jbosh.BOSHClientConfig; |
| import com.kenai.jbosh.BOSHClientConnEvent; |
| import com.kenai.jbosh.BOSHClientConnListener; |
| import com.kenai.jbosh.BOSHClientRequestListener; |
| import com.kenai.jbosh.BOSHClientResponseListener; |
| import com.kenai.jbosh.BOSHException; |
| import com.kenai.jbosh.BOSHMessageEvent; |
| import com.kenai.jbosh.BodyQName; |
| import com.kenai.jbosh.ComposableBody; |
| |
| /** |
| * Creates a connection to a XMPP server via HTTP binding. |
| * This is specified in the XEP-0206: XMPP Over BOSH. |
| * |
| * @see Connection |
| * @author Guenther Niess |
| */ |
| public class BOSHConnection extends Connection { |
| |
| /** |
| * 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 Thread environment for sending packet listeners. |
| */ |
| private ExecutorService listenerExecutor; |
| |
| // 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; |
| |
| /** |
| * The roster maybe also called buddy list holds the list of the users contacts. |
| */ |
| private Roster roster = 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 BOSHConnection(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 BOSHConnection(BOSHConfiguration config) { |
| super(config); |
| this.config = config; |
| } |
| |
| public void connect() throws XMPPException { |
| 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; |
| } |
| saslAuthentication.init(); |
| 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()); |
| |
| // Create an executor to deliver incoming packets to listeners. |
| // We'll use a single thread with an unbounded queue. |
| listenerExecutor = Executors |
| .newSingleThreadExecutor(new ThreadFactory() { |
| public Thread newThread(Runnable runnable) { |
| Thread thread = new Thread(runnable, |
| "Smack Listener Processor (" |
| + connectionCounterValue + ")"); |
| thread.setDaemon(true); |
| return thread; |
| } |
| }); |
| 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 XMPPException("Can't connect to " + getServiceName(), e); |
| } |
| |
| // Wait for the response from the server |
| synchronized (this) { |
| long endTime = System.currentTimeMillis() + |
| SmackConfiguration.getPacketReplyTimeout() * 6; |
| while ((!connected) && (System.currentTimeMillis() < endTime)) { |
| try { |
| wait(Math.abs(endTime - System.currentTimeMillis())); |
| } |
| 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 XMPPException( |
| errorMessage, |
| new XMPPError(XMPPError.Condition.remote_server_timeout, errorMessage)); |
| } |
| } |
| |
| public String getConnectionID() { |
| if (!connected) { |
| return null; |
| } else if (authID != null) { |
| return authID; |
| } else { |
| return sessionID; |
| } |
| } |
| |
| public Roster getRoster() { |
| if (roster == null) { |
| return null; |
| } |
| if (!config.isRosterLoadedAtLogin()) { |
| roster.reload(); |
| } |
| // If this is the first time the user has asked for the roster after calling |
| // login, we want to wait for the server to send back the user's roster. |
| // This behavior shields API users from having to worry about the fact that |
| // roster operations are asynchronous, although they'll still have to listen |
| // for changes to the roster. Note: because of this waiting logic, internal |
| // Smack code should be wary about calling the getRoster method, and may |
| // need to access the roster object directly. |
| if (!roster.rosterInitialized) { |
| try { |
| synchronized (roster) { |
| long waitTime = SmackConfiguration.getPacketReplyTimeout(); |
| long start = System.currentTimeMillis(); |
| while (!roster.rosterInitialized) { |
| if (waitTime <= 0) { |
| break; |
| } |
| roster.wait(waitTime); |
| long now = System.currentTimeMillis(); |
| waitTime -= now - start; |
| start = now; |
| } |
| } |
| } catch (InterruptedException ie) { |
| // Ignore. |
| } |
| } |
| return roster; |
| } |
| |
| 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 { |
| if (!isConnected()) { |
| throw new IllegalStateException("Not connected to server."); |
| } |
| if (authenticated) { |
| throw new IllegalStateException("Already logged in to server."); |
| } |
| // Do partial version of nameprep on the username. |
| username = username.toLowerCase().trim(); |
| |
| String response; |
| if (config.isSASLAuthenticationEnabled() |
| && saslAuthentication.hasNonAnonymousAuthentication()) { |
| // Authenticate using SASL |
| if (password != null) { |
| response = saslAuthentication.authenticate(username, password, resource); |
| } else { |
| response = saslAuthentication.authenticate(username, resource, config.getCallbackHandler()); |
| } |
| } else { |
| // Authenticate using Non-SASL |
| response = new NonSASLAuthentication(this).authenticate(username, password, 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; |
| } |
| } |
| |
| // Create the roster if it is not a reconnection. |
| if (this.roster == null) { |
| if (this.rosterStorage == null) { |
| this.roster = new Roster(this); |
| } else { |
| this.roster = new Roster(this, rosterStorage); |
| } |
| } |
| |
| // Set presence to online. |
| if (config.isSendPresence()) { |
| sendPacket(new Presence(Presence.Type.available)); |
| } |
| |
| // Indicate that we're now authenticated. |
| authenticated = true; |
| anonymous = false; |
| |
| if (config.isRosterLoadedAtLogin()) { |
| this.roster.reload(); |
| } |
| // 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); |
| } |
| } |
| |
| public void loginAnonymously() throws XMPPException { |
| if (!isConnected()) { |
| throw new IllegalStateException("Not connected to server."); |
| } |
| if (authenticated) { |
| throw new IllegalStateException("Already logged in to server."); |
| } |
| |
| String response; |
| if (config.isSASLAuthenticationEnabled() && |
| saslAuthentication.hasAnonymousAuthentication()) { |
| response = saslAuthentication.authenticateAnonymously(); |
| } |
| else { |
| // Authenticate using Non-SASL |
| response = new NonSASLAuthentication(this).authenticateAnonymously(); |
| } |
| |
| // Set the user value. |
| this.user = response; |
| // Update the serviceName with the one returned by the server |
| config.setServiceName(StringUtils.parseServer(response)); |
| |
| // Anonymous users can't have a roster. |
| roster = null; |
| |
| // 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); |
| } |
| } |
| |
| public void sendPacket(Packet packet) { |
| if (!isConnected()) { |
| throw new IllegalStateException("Not connected to server."); |
| } |
| if (packet == null) { |
| throw new NullPointerException("Packet is null."); |
| } |
| if (!done) { |
| // Invoke interceptors for the new packet that is about to be sent. |
| // Interceptors |
| // may modify the content of the packet. |
| firePacketInterceptors(packet); |
| |
| try { |
| send(ComposableBody.builder().setPayloadXML(packet.toXML()) |
| .build()); |
| } catch (BOSHException e) { |
| e.printStackTrace(); |
| return; |
| } |
| |
| // Process packet writer listeners. Note that we're using the |
| // sending |
| // thread so it's expected that listeners are fast. |
| firePacketSendingListeners(packet); |
| } |
| } |
| |
| public void disconnect(Presence unavailablePresence) { |
| if (!connected) { |
| return; |
| } |
| shutdown(unavailablePresence); |
| |
| // Cleanup |
| if (roster != null) { |
| roster.cleanup(); |
| roster = null; |
| } |
| sendListeners.clear(); |
| recvListeners.clear(); |
| collectors.clear(); |
| interceptors.clear(); |
| |
| // Reset the connection flags |
| wasAuthenticated = false; |
| isFirstInitialization = true; |
| |
| // Notify connection listeners of the connection closing if done hasn't already been set. |
| for (ConnectionListener listener : getConnectionListeners()) { |
| try { |
| listener.connectionClosed(); |
| } |
| catch (Exception e) { |
| // Catch and print any exception so we can recover |
| // from a faulty listener and finish the shutdown process |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| /** |
| * 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()) |
| .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; |
| } |
| |
| // Shut down the listener executor. |
| if (listenerExecutor != null) { |
| listenerExecutor.shutdown(); |
| } |
| 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); |
| } |
| |
| /** |
| * Processes a packet after it's been fully parsed by looping through the |
| * installed packet collectors and listeners and letting them examine the |
| * packet to see if they are a match with the filter. |
| * |
| * @param packet the packet to process. |
| */ |
| protected void processPacket(Packet packet) { |
| if (packet == null) { |
| return; |
| } |
| |
| // Loop through all collectors and notify the appropriate ones. |
| for (PacketCollector collector : getPacketCollectors()) { |
| collector.processPacket(packet); |
| } |
| |
| // Deliver the incoming packet to listeners. |
| listenerExecutor.submit(new ListenerNotification(packet)); |
| } |
| |
| /** |
| * 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)); |
| // Print the stack trace to help catch the problem |
| e.printStackTrace(); |
| // Notify connection listeners of the error. |
| for (ConnectionListener listener : getConnectionListeners()) { |
| try { |
| listener.connectionClosedOnError(e); |
| } |
| catch (Exception e2) { |
| // Catch and print any exception so we can recover |
| // from a faulty listener |
| e2.printStackTrace(); |
| } |
| } |
| } |
| |
| |
| /** |
| * 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 BOSHConnection connection; |
| |
| public BOSHConnectionListener(BOSHConnection 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 (XMPPException e) { |
| for (ConnectionListener listener : getConnectionListeners()) { |
| listener.reconnectionFailed(e); |
| } |
| } |
| } |
| } |
| else { |
| if (connEvent.isError()) { |
| try { |
| connEvent.getCause(); |
| } |
| catch (Exception e) { |
| notifyConnectionError(e); |
| } |
| } |
| connected = false; |
| } |
| } |
| finally { |
| synchronized (connection) { |
| connection.notifyAll(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * This class notifies all listeners that a packet was received. |
| */ |
| private class ListenerNotification implements Runnable { |
| |
| private Packet packet; |
| |
| public ListenerNotification(Packet packet) { |
| this.packet = packet; |
| } |
| |
| public void run() { |
| for (ListenerWrapper listenerWrapper : recvListeners.values()) { |
| listenerWrapper.notifyListener(packet); |
| } |
| } |
| } |
| |
| @Override |
| public void setRosterStorage(RosterStorage storage) |
| throws IllegalStateException { |
| if(this.roster!=null){ |
| throw new IllegalStateException("Roster is already initialized"); |
| } |
| this.rosterStorage = storage; |
| } |
| } |