aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smack/XMPPConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/jivesoftware/smack/XMPPConnection.java')
-rw-r--r--src/org/jivesoftware/smack/XMPPConnection.java1116
1 files changed, 1116 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smack/XMPPConnection.java b/src/org/jivesoftware/smack/XMPPConnection.java
new file mode 100644
index 0000000..badf29c
--- /dev/null
+++ b/src/org/jivesoftware/smack/XMPPConnection.java
@@ -0,0 +1,1116 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.filter.PacketFilter;
+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 org.jivesoftware.smack.util.dns.HostAddress;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Constructor;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.Security;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Creates a socket connection to a XMPP server. This is the default connection
+ * to a Jabber server and is specified in the XMPP Core (RFC 3920).
+ *
+ * @see Connection
+ * @author Matt Tucker
+ */
+public class XMPPConnection extends Connection {
+
+ /**
+ * The socket which is used for this connection.
+ */
+ Socket socket;
+
+ String connectionID = null;
+ private String user = null;
+ private boolean connected = false;
+ // socketClosed is used concurrent
+ // by XMPPConnection, PacketReader, PacketWriter
+ private volatile boolean socketClosed = false;
+
+ /**
+ * Flag that indicates if the user is currently authenticated with the server.
+ */
+ private boolean authenticated = false;
+ /**
+ * Flag that indicates if the user was authenticated with the server when the connection
+ * to the server was closed (abruptly or not).
+ */
+ private boolean wasAuthenticated = false;
+ private boolean anonymous = false;
+ private boolean usingTLS = false;
+
+ PacketWriter packetWriter;
+ PacketReader packetReader;
+
+ Roster roster = null;
+
+ /**
+ * Collection of available stream compression methods offered by the server.
+ */
+ private Collection<String> compressionMethods;
+
+ /**
+ * Set to true by packet writer if the server acknowledged the compression
+ */
+ private boolean serverAckdCompression = false;
+
+ /**
+ * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
+ * performed to determine the IP address and port corresponding to the
+ * service name; if that lookup fails, it's assumed that server resides at
+ * <tt>serviceName</tt> with the default port of 5222. Encrypted connections (TLS)
+ * will be used if available, stream compression is disabled, and standard SASL
+ * mechanisms will be used for authentication.<p>
+ * <p/>
+ * This is the simplest constructor for connecting to an XMPP server. Alternatively,
+ * you can get fine-grained control over connection settings using the
+ * {@link #XMPPConnection(ConnectionConfiguration)} constructor.<p>
+ * <p/>
+ * Note that XMPPConnection constructors do not establish a connection to the server
+ * and you must call {@link #connect()}.<p>
+ * <p/>
+ * The CallbackHandler will only be used if the connection requires the client provide
+ * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
+ * to prompt for a password to unlock the keystore containing the SSL certificate.
+ *
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
+ * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
+ */
+ public XMPPConnection(String serviceName, CallbackHandler callbackHandler) {
+ // Create the configuration for this new connection
+ super(new ConnectionConfiguration(serviceName));
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ config.setCallbackHandler(callbackHandler);
+ }
+
+ /**
+ * Creates a new XMPP connection in the same way {@link #XMPPConnection(String,CallbackHandler)} does, but
+ * with no callback handler for password prompting of the keystore. This will work
+ * in most cases, provided the client is not required to provide a certificate to
+ * the server.
+ *
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
+ */
+ public XMPPConnection(String serviceName) {
+ // Create the configuration for this new connection
+ super(new ConnectionConfiguration(serviceName));
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ }
+
+ /**
+ * Creates a new XMPP connection in the same way {@link #XMPPConnection(ConnectionConfiguration,CallbackHandler)} does, but
+ * with no callback handler for password prompting of the keystore. This will work
+ * in most cases, provided the client is not required to provide a certificate to
+ * the server.
+ *
+ *
+ * @param config the connection configuration.
+ */
+ public XMPPConnection(ConnectionConfiguration config) {
+ super(config);
+ }
+
+ /**
+ * Creates a new XMPP connection using the specified connection configuration.<p>
+ * <p/>
+ * Manually specifying connection configuration information is suitable for
+ * advanced users of the API. In many cases, using the
+ * {@link #XMPPConnection(String)} constructor is a better approach.<p>
+ * <p/>
+ * Note that XMPPConnection constructors do not establish a connection to the server
+ * and you must call {@link #connect()}.<p>
+ * <p/>
+ *
+ * The CallbackHandler will only be used if the connection requires the client provide
+ * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
+ * to prompt for a password to unlock the keystore containing the SSL certificate.
+ *
+ * @param config the connection configuration.
+ * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
+ */
+ public XMPPConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) {
+ super(config);
+ config.setCallbackHandler(callbackHandler);
+ }
+
+ public String getConnectionID() {
+ if (!isConnected()) {
+ return null;
+ }
+ return connectionID;
+ }
+
+ public String getUser() {
+ if (!isAuthenticated()) {
+ return null;
+ }
+ return user;
+ }
+
+ @Override
+ public synchronized 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;
+ }
+ }
+
+ // If compression is enabled then request the server to use stream compression
+ if (config.isCompressionEnabled()) {
+ useCompression();
+ }
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+ anonymous = false;
+
+ // Create the roster if it is not a reconnection or roster already created by getRoster()
+ if (this.roster == null) {
+ if(rosterStorage==null){
+ this.roster = new Roster(this);
+ }
+ else{
+ this.roster = new Roster(this,rosterStorage);
+ }
+ }
+ if (config.isRosterLoadedAtLogin()) {
+ this.roster.reload();
+ }
+
+ // Set presence to online.
+ if (config.isSendPresence()) {
+ packetWriter.sendPacket(new Presence(Presence.Type.available));
+ }
+
+ // Stores the authentication 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.
+ // 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);
+ }
+ }
+
+ @Override
+ public synchronized 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));
+
+ // If compression is enabled then request the server to use stream compression
+ if (config.isCompressionEnabled()) {
+ useCompression();
+ }
+
+ // Set presence to online.
+ packetWriter.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 Roster getRoster() {
+ // synchronize against login()
+ synchronized(this) {
+ // if connection is authenticated the roster is already set by login()
+ // or a previous call to getRoster()
+ if (!isAuthenticated() || isAnonymous()) {
+ if (roster == null) {
+ roster = new Roster(this);
+ }
+ return roster;
+ }
+ }
+
+ 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 boolean isConnected() {
+ return connected;
+ }
+
+ public boolean isSecureConnection() {
+ return isUsingTLS();
+ }
+
+ public boolean isSocketClosed() {
+ return socketClosed;
+ }
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public boolean isAnonymous() {
+ return anonymous;
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable then closing the stream to
+ * the XMPP server. The shutdown logic will be used during a planned disconnection or when
+ * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
+ * packet reader, packet writer, 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) {
+ // Set presence to offline.
+ if (packetWriter != null) {
+ packetWriter.sendPacket(unavailablePresence);
+ }
+
+ this.setWasAuthenticated(authenticated);
+ authenticated = false;
+
+ if (packetReader != null) {
+ packetReader.shutdown();
+ }
+ if (packetWriter != null) {
+ packetWriter.shutdown();
+ }
+
+ // Wait 150 ms for processes to clean-up, then shutdown.
+ try {
+ Thread.sleep(150);
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+
+ // Set socketClosed to true. This will cause the PacketReader
+ // and PacketWriter to ingore any Exceptions that are thrown
+ // because of a read/write from/to a closed stream.
+ // It is *important* that this is done before socket.close()!
+ socketClosed = true;
+ try {
+ socket.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ // In most cases the close() should be successful, so set
+ // connected to false here.
+ connected = false;
+
+ // Close down the readers and writers.
+ if (reader != null) {
+ try {
+ // Should already be closed by the previous
+ // socket.close(). But just in case do it explicitly.
+ reader.close();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ reader = null;
+ }
+ if (writer != null) {
+ try {
+ // Should already be closed by the previous
+ // socket.close(). But just in case do it explicitly.
+ writer.close();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ writer = null;
+ }
+
+ // Make sure that the socket is really closed
+ try {
+ // Does nothing if the socket is already closed
+ socket.close();
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+
+ saslAuthentication.init();
+ }
+
+ public synchronized void disconnect(Presence unavailablePresence) {
+ // If not connected, ignore this request.
+ PacketReader packetReader = this.packetReader;
+ PacketWriter packetWriter = this.packetWriter;
+ if (packetReader == null || packetWriter == null) {
+ return;
+ }
+
+ if (!isConnected()) {
+ return;
+ }
+
+ shutdown(unavailablePresence);
+
+ if (roster != null) {
+ roster.cleanup();
+ roster = null;
+ }
+ chatManager = null;
+
+ wasAuthenticated = false;
+
+ packetWriter.cleanup();
+ packetReader.cleanup();
+ }
+
+ public void sendPacket(Packet packet) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (packet == null) {
+ throw new NullPointerException("Packet is null.");
+ }
+ packetWriter.sendPacket(packet);
+ }
+
+ /**
+ * Registers a packet interceptor with this connection. The interceptor will be
+ * invoked every time a packet is about to be sent by this connection. Interceptors
+ * may modify the packet to be sent. A packet filter determines which packets
+ * will be delivered to the interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+ * @param packetFilter the packet filter to use.
+ * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}.
+ */
+ public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
+ PacketFilter packetFilter) {
+ addPacketInterceptor(packetInterceptor, packetFilter);
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}.
+ */
+ public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
+ removePacketInterceptor(packetInterceptor);
+ }
+
+ /**
+ * Registers a packet listener with this connection. The listener will be
+ * notified of every packet that this connection sends. A packet filter determines
+ * which packets will be delivered to the listener. Note that the thread
+ * that writes packets will be used to invoke the listeners. Therefore, each
+ * packet listener should complete all operations quickly or use a different
+ * thread for processing.
+ *
+ * @param packetListener the packet listener to notify of sent packets.
+ * @param packetFilter the packet filter to use.
+ * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}.
+ */
+ public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
+ addPacketSendingListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Removes a packet listener for sending packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}.
+ */
+ public void removePacketWriterListener(PacketListener packetListener) {
+ removePacketSendingListener(packetListener);
+ }
+
+ private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
+ XMPPException exception = null;
+ Iterator<HostAddress> it = config.getHostAddresses().iterator();
+ List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
+ boolean xmppIOError = false;
+ while (it.hasNext()) {
+ exception = null;
+ HostAddress hostAddress = it.next();
+ String host = hostAddress.getFQDN();
+ int port = hostAddress.getPort();
+ try {
+ if (config.getSocketFactory() == null) {
+ this.socket = new Socket(host, port);
+ }
+ else {
+ this.socket = config.getSocketFactory().createSocket(host, port);
+ }
+ } catch (UnknownHostException uhe) {
+ String errorMessage = "Could not connect to " + host + ":" + port + ".";
+ exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout,
+ errorMessage), uhe);
+ } catch (IOException ioe) {
+ String errorMessage = "XMPPError connecting to " + host + ":" + port + ".";
+ exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error,
+ errorMessage), ioe);
+ xmppIOError = true;
+ }
+ if (exception == null) {
+ // We found a host to connect to, break here
+ config.setUsedHostAddress(hostAddress);
+ break;
+ }
+ hostAddress.setException(exception);
+ failedAddresses.add(hostAddress);
+ if (!it.hasNext()) {
+ // There are no more host addresses to try
+ // throw an exception and report all tried
+ // HostAddresses in the exception
+ StringBuilder sb = new StringBuilder();
+ for (HostAddress fha : failedAddresses) {
+ sb.append(fha.getErrorMessage());
+ sb.append("; ");
+ }
+ XMPPError xmppError;
+ if (xmppIOError) {
+ xmppError = new XMPPError(XMPPError.Condition.remote_server_error);
+ }
+ else {
+ xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout);
+ }
+ throw new XMPPException(sb.toString(), xmppError);
+ }
+ }
+ socketClosed = false;
+ initConnection();
+ }
+
+ /**
+ * Initializes the connection by creating a packet reader and writer and opening a
+ * XMPP stream to the server.
+ *
+ * @throws XMPPException if establishing a connection to the server fails.
+ */
+ private void initConnection() throws XMPPException {
+ boolean isFirstInitialization = packetReader == null || packetWriter == null;
+ compressionHandler = null;
+ serverAckdCompression = false;
+
+ // Set the reader and writer instance variables
+ initReaderAndWriter();
+
+ try {
+ if (isFirstInitialization) {
+ packetWriter = new PacketWriter(this);
+ packetReader = new PacketReader(this);
+
+ // If debugging is enabled, we should start the thread that will listen for
+ // all packets and then log them.
+ if (config.isDebuggerEnabled()) {
+ addPacketListener(debugger.getReaderListener(), null);
+ if (debugger.getWriterListener() != null) {
+ addPacketSendingListener(debugger.getWriterListener(), null);
+ }
+ }
+ }
+ else {
+ packetWriter.init();
+ packetReader.init();
+ }
+
+ // Start the packet writer. This will open a XMPP stream to the server
+ packetWriter.startup();
+ // Start the packet reader. The startup() method will block until we
+ // get an opening stream packet back from server.
+ packetReader.startup();
+
+ // Make note of the fact that we're now connected.
+ connected = true;
+
+ if (isFirstInitialization) {
+ // Notify listeners that a new connection has been established
+ for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
+ listener.connectionCreated(this);
+ }
+ }
+ else if (!wasAuthenticated) {
+ notifyReconnection();
+ }
+
+ }
+ catch (XMPPException ex) {
+ // An exception occurred in setting up the connection. Make sure we shut down the
+ // readers and writers and close the socket.
+
+ if (packetWriter != null) {
+ try {
+ packetWriter.shutdown();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ packetWriter = null;
+ }
+ if (packetReader != null) {
+ try {
+ packetReader.shutdown();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ packetReader = 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;
+ }
+ if (socket != null) {
+ try {
+ socket.close();
+ }
+ catch (Exception e) { /* ignore */ }
+ socket = null;
+ }
+ this.setWasAuthenticated(authenticated);
+ chatManager = null;
+ authenticated = false;
+ connected = false;
+
+ throw ex; // Everything stoppped. Now throw the exception.
+ }
+ }
+
+ private void initReaderAndWriter() throws XMPPException {
+ try {
+ if (compressionHandler == null) {
+ reader =
+ new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
+ writer = new BufferedWriter(
+ new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+ }
+ else {
+ try {
+ OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
+ writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+
+ InputStream is = compressionHandler.getInputStream(socket.getInputStream());
+ reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ compressionHandler = null;
+ reader = new BufferedReader(
+ new InputStreamReader(socket.getInputStream(), "UTF-8"));
+ writer = new BufferedWriter(
+ new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+ }
+ }
+ }
+ catch (IOException ioe) {
+ throw new XMPPException(
+ "XMPPError establishing connection with server.",
+ new XMPPError(XMPPError.Condition.remote_server_error,
+ "XMPPError establishing connection with server."),
+ ioe);
+ }
+
+ // If debugging is enabled, we open a window and write out all network traffic.
+ initDebugger();
+ }
+
+ /***********************************************
+ * TLS code below
+ **********************************************/
+
+ /**
+ * Returns true if the connection to the server has successfully negotiated TLS. Once TLS
+ * has been negotiatied the connection has been secured.
+ *
+ * @return true if the connection to the server has successfully negotiated TLS.
+ */
+ public boolean isUsingTLS() {
+ return usingTLS;
+ }
+
+ /**
+ * Notification message saying that the server supports TLS so confirm the server that we
+ * want to secure the connection.
+ *
+ * @param required true when the server indicates that TLS is required.
+ */
+ void startTLSReceived(boolean required) {
+ if (required && config.getSecurityMode() ==
+ ConnectionConfiguration.SecurityMode.disabled) {
+ notifyConnectionError(new IllegalStateException(
+ "TLS required by server but not allowed by connection configuration"));
+ return;
+ }
+
+ if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
+ // Do not secure the connection using TLS since TLS was disabled
+ return;
+ }
+ try {
+ writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
+ writer.flush();
+ }
+ catch (IOException e) {
+ notifyConnectionError(e);
+ }
+ }
+
+ /**
+ * The server has indicated that TLS negotiation can start. We now need to secure the
+ * existing plain connection and perform a handshake. This method won't return until the
+ * connection has finished the handshake or an error occured while securing the connection.
+ *
+ * @throws Exception if an exception occurs.
+ */
+ void proceedTLSReceived() throws Exception {
+ SSLContext context = this.config.getCustomSSLContext();
+ KeyStore ks = null;
+ KeyManager[] kms = null;
+ PasswordCallback pcb = null;
+
+ if(config.getCallbackHandler() == null) {
+ ks = null;
+ } else if (context == null) {
+ //System.out.println("Keystore type: "+configuration.getKeystoreType());
+ if(config.getKeystoreType().equals("NONE")) {
+ ks = null;
+ pcb = null;
+ }
+ else if(config.getKeystoreType().equals("PKCS11")) {
+ try {
+ Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
+ String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library();
+ ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes());
+ Provider p = (Provider)c.newInstance(config);
+ Security.addProvider(p);
+ ks = KeyStore.getInstance("PKCS11",p);
+ pcb = new PasswordCallback("PKCS11 Password: ",false);
+ this.config.getCallbackHandler().handle(new Callback[]{pcb});
+ ks.load(null,pcb.getPassword());
+ }
+ catch (Exception e) {
+ ks = null;
+ pcb = null;
+ }
+ }
+ else if(config.getKeystoreType().equals("Apple")) {
+ ks = KeyStore.getInstance("KeychainStore","Apple");
+ ks.load(null,null);
+ //pcb = new PasswordCallback("Apple Keychain",false);
+ //pcb.setPassword(null);
+ }
+ else {
+ ks = KeyStore.getInstance(config.getKeystoreType());
+ try {
+ pcb = new PasswordCallback("Keystore Password: ",false);
+ config.getCallbackHandler().handle(new Callback[]{pcb});
+ ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword());
+ }
+ catch(Exception e) {
+ ks = null;
+ pcb = null;
+ }
+ }
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ try {
+ if(pcb == null) {
+ kmf.init(ks,null);
+ } else {
+ kmf.init(ks,pcb.getPassword());
+ pcb.clearPassword();
+ }
+ kms = kmf.getKeyManagers();
+ } catch (NullPointerException npe) {
+ kms = null;
+ }
+ }
+
+ // Verify certificate presented by the server
+ if (context == null) {
+ context = SSLContext.getInstance("TLS");
+ context.init(kms, new javax.net.ssl.TrustManager[] { new ServerTrustManager(getServiceName(), config) },
+ new java.security.SecureRandom());
+ }
+ Socket plain = socket;
+ // Secure the plain connection
+ socket = context.getSocketFactory().createSocket(plain,
+ plain.getInetAddress().getHostAddress(), plain.getPort(), true);
+ socket.setSoTimeout(0);
+ socket.setKeepAlive(true);
+ // Initialize the reader and writer with the new secured version
+ initReaderAndWriter();
+ // Proceed to do the handshake
+ ((SSLSocket) socket).startHandshake();
+ //if (((SSLSocket) socket).getWantClientAuth()) {
+ // System.err.println("Connection wants client auth");
+ //}
+ //else if (((SSLSocket) socket).getNeedClientAuth()) {
+ // System.err.println("Connection needs client auth");
+ //}
+ //else {
+ // System.err.println("Connection does not require client auth");
+ // }
+ // Set that TLS was successful
+ usingTLS = true;
+
+ // Set the new writer to use
+ packetWriter.setWriter(writer);
+ // Send a new opening stream to the server
+ packetWriter.openStream();
+ }
+
+ /**
+ * Sets the available stream compression methods offered by the server.
+ *
+ * @param methods compression methods offered by the server.
+ */
+ void setAvailableCompressionMethods(Collection<String> methods) {
+ compressionMethods = methods;
+ }
+
+ /**
+ * Returns the compression handler that can be used for one compression methods offered by the server.
+ *
+ * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
+ *
+ */
+ private XMPPInputOutputStream maybeGetCompressionHandler() {
+ if (compressionMethods != null) {
+ for (XMPPInputOutputStream handler : compressionHandlers) {
+ if (!handler.isSupported())
+ continue;
+
+ String method = handler.getCompressionMethod();
+ if (compressionMethods.contains(method))
+ return handler;
+ }
+ }
+ return null;
+ }
+
+ public boolean isUsingCompression() {
+ return compressionHandler != null && serverAckdCompression;
+ }
+
+ /**
+ * Starts using stream compression that will compress network traffic. Traffic can be
+ * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
+ * connection. However, the server and the client will need to use more CPU time in order to
+ * un/compress network data so under high load the server performance might be affected.<p>
+ * <p/>
+ * Stream compression has to have been previously offered by the server. Currently only the
+ * zlib method is supported by the client. Stream compression negotiation has to be done
+ * before authentication took place.<p>
+ * <p/>
+ * Note: to use stream compression the smackx.jar file has to be present in the classpath.
+ *
+ * @return true if stream compression negotiation was successful.
+ */
+ private boolean useCompression() {
+ // If stream compression was offered by the server and we want to use
+ // compression then send compression request to the server
+ if (authenticated) {
+ throw new IllegalStateException("Compression should be negotiated before authentication.");
+ }
+
+ if ((compressionHandler = maybeGetCompressionHandler()) != null) {
+ requestStreamCompression(compressionHandler.getCompressionMethod());
+ // Wait until compression is being used or a timeout happened
+ synchronized (this) {
+ try {
+ this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
+ }
+ catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+ return isUsingCompression();
+ }
+ return false;
+ }
+
+ /**
+ * Request the server that we want to start using stream compression. When using TLS
+ * then negotiation of stream compression can only happen after TLS was negotiated. If TLS
+ * compression is being used the stream compression should not be used.
+ */
+ private void requestStreamCompression(String method) {
+ try {
+ writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
+ writer.write("<method>" + method + "</method></compress>");
+ writer.flush();
+ }
+ catch (IOException e) {
+ notifyConnectionError(e);
+ }
+ }
+
+ /**
+ * Start using stream compression since the server has acknowledged stream compression.
+ *
+ * @throws Exception if there is an exception starting stream compression.
+ */
+ void startStreamCompression() throws Exception {
+ serverAckdCompression = true;
+ // Initialize the reader and writer with the new secured version
+ initReaderAndWriter();
+
+ // Set the new writer to use
+ packetWriter.setWriter(writer);
+ // Send a new opening stream to the server
+ packetWriter.openStream();
+ // Notify that compression is being used
+ synchronized (this) {
+ this.notify();
+ }
+ }
+
+ /**
+ * Notifies the XMPP connection that stream compression was denied so that
+ * the connection process can proceed.
+ */
+ void streamCompressionDenied() {
+ synchronized (this) {
+ this.notify();
+ }
+ }
+
+ /**
+ * Establishes a connection to the XMPP server and performs an automatic login
+ * only if the previous connection state was logged (authenticated). It basically
+ * creates and maintains a socket connection to the server.<p>
+ * <p/>
+ * Listeners will be preserved from a previous connection if the reconnection
+ * occurs after an abrupt termination.
+ *
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropriate error messages to end-users.
+ */
+ public void connect() throws XMPPException {
+ // Establishes the connection, readers and writers
+ connectUsingConfiguration(config);
+ // Automatically makes the login if the user was previously connected successfully
+ // to the server and the connection was terminated abruptly
+ if (connected && wasAuthenticated) {
+ // Make the login
+ if (isAnonymous()) {
+ // Make the anonymous login
+ loginAnonymously();
+ }
+ else {
+ login(config.getUsername(), config.getPassword(), config.getResource());
+ }
+ notifyReconnection();
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ @Override
+ public void setRosterStorage(RosterStorage storage)
+ throws IllegalStateException {
+ if(roster!=null){
+ throw new IllegalStateException("Roster is already initialized");
+ }
+ this.rosterStorage = storage;
+ }
+
+ /**
+ * Sends out a notification that there was an error with the connection
+ * and closes the connection. Also prints the stack trace of the given exception
+ *
+ * @param e the exception that causes the connection close event.
+ */
+ synchronized void notifyConnectionError(Exception e) {
+ // Listeners were already notified of the exception, return right here.
+ if (packetReader.done && packetWriter.done) return;
+
+ packetReader.done = true;
+ packetWriter.done = true;
+ // 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();
+ }
+ }
+ }
+
+
+ /**
+ * Sends a notification indicating that the connection was reconnected successfully.
+ */
+ protected void notifyReconnection() {
+ // Notify connection listeners of the reconnection.
+ for (ConnectionListener listener : getConnectionListeners()) {
+ try {
+ listener.reconnectionSuccessful();
+ }
+ catch (Exception e) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener
+ e.printStackTrace();
+ }
+ }
+ }
+}