Home | History | Annotate | Download | only in smack
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2003-2007 Jive Software.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 
     21 package org.jivesoftware.smack;
     22 
     23 import org.jivesoftware.smack.compression.XMPPInputOutputStream;
     24 import org.jivesoftware.smack.filter.PacketFilter;
     25 import org.jivesoftware.smack.packet.Packet;
     26 import org.jivesoftware.smack.packet.Presence;
     27 import org.jivesoftware.smack.packet.XMPPError;
     28 import org.jivesoftware.smack.util.StringUtils;
     29 import org.jivesoftware.smack.util.dns.HostAddress;
     30 
     31 import javax.net.ssl.KeyManager;
     32 import javax.net.ssl.KeyManagerFactory;
     33 import javax.net.ssl.SSLContext;
     34 import javax.net.ssl.SSLSocket;
     35 import org.apache.harmony.javax.security.auth.callback.Callback;
     36 import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
     37 import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
     38 
     39 import java.io.BufferedReader;
     40 import java.io.BufferedWriter;
     41 import java.io.ByteArrayInputStream;
     42 import java.io.FileInputStream;
     43 import java.io.IOException;
     44 import java.io.InputStream;
     45 import java.io.InputStreamReader;
     46 import java.io.OutputStream;
     47 import java.io.OutputStreamWriter;
     48 import java.lang.reflect.Constructor;
     49 import java.net.Socket;
     50 import java.net.UnknownHostException;
     51 import java.security.KeyStore;
     52 import java.security.Provider;
     53 import java.security.Security;
     54 import java.util.Collection;
     55 import java.util.Iterator;
     56 import java.util.LinkedList;
     57 import java.util.List;
     58 
     59 /**
     60  * Creates a socket connection to a XMPP server. This is the default connection
     61  * to a Jabber server and is specified in the XMPP Core (RFC 3920).
     62  *
     63  * @see Connection
     64  * @author Matt Tucker
     65  */
     66 public class XMPPConnection extends Connection {
     67 
     68     /**
     69      * The socket which is used for this connection.
     70      */
     71     Socket socket;
     72 
     73     String connectionID = null;
     74     private String user = null;
     75     private boolean connected = false;
     76     // socketClosed is used concurrent
     77     // by XMPPConnection, PacketReader, PacketWriter
     78     private volatile boolean socketClosed = false;
     79 
     80     /**
     81      * Flag that indicates if the user is currently authenticated with the server.
     82      */
     83     private boolean authenticated = false;
     84     /**
     85      * Flag that indicates if the user was authenticated with the server when the connection
     86      * to the server was closed (abruptly or not).
     87      */
     88     private boolean wasAuthenticated = false;
     89     private boolean anonymous = false;
     90     private boolean usingTLS = false;
     91 
     92     PacketWriter packetWriter;
     93     PacketReader packetReader;
     94 
     95     Roster roster = null;
     96 
     97     /**
     98      * Collection of available stream compression methods offered by the server.
     99      */
    100     private Collection<String> compressionMethods;
    101 
    102     /**
    103      * Set to true by packet writer if the server acknowledged the compression
    104      */
    105     private boolean serverAckdCompression = false;
    106 
    107     /**
    108      * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
    109      * performed to determine the IP address and port corresponding to the
    110      * service name; if that lookup fails, it's assumed that server resides at
    111      * <tt>serviceName</tt> with the default port of 5222. Encrypted connections (TLS)
    112      * will be used if available, stream compression is disabled, and standard SASL
    113      * mechanisms will be used for authentication.<p>
    114      * <p/>
    115      * This is the simplest constructor for connecting to an XMPP server. Alternatively,
    116      * you can get fine-grained control over connection settings using the
    117      * {@link #XMPPConnection(ConnectionConfiguration)} constructor.<p>
    118      * <p/>
    119      * Note that XMPPConnection constructors do not establish a connection to the server
    120      * and you must call {@link #connect()}.<p>
    121      * <p/>
    122      * The CallbackHandler will only be used if the connection requires the client provide
    123      * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
    124      * to prompt for a password to unlock the keystore containing the SSL certificate.
    125      *
    126      * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
    127      * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
    128      */
    129     public XMPPConnection(String serviceName, CallbackHandler callbackHandler) {
    130         // Create the configuration for this new connection
    131         super(new ConnectionConfiguration(serviceName));
    132         config.setCompressionEnabled(false);
    133         config.setSASLAuthenticationEnabled(true);
    134         config.setDebuggerEnabled(DEBUG_ENABLED);
    135         config.setCallbackHandler(callbackHandler);
    136     }
    137 
    138     /**
    139      * Creates a new XMPP connection in the same way {@link #XMPPConnection(String,CallbackHandler)} does, but
    140      * with no callback handler for password prompting of the keystore.  This will work
    141      * in most cases, provided the client is not required to provide a certificate to
    142      * the server.
    143      *
    144      * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
    145      */
    146     public XMPPConnection(String serviceName) {
    147         // Create the configuration for this new connection
    148         super(new ConnectionConfiguration(serviceName));
    149         config.setCompressionEnabled(false);
    150         config.setSASLAuthenticationEnabled(true);
    151         config.setDebuggerEnabled(DEBUG_ENABLED);
    152     }
    153 
    154     /**
    155      * Creates a new XMPP connection in the same way {@link #XMPPConnection(ConnectionConfiguration,CallbackHandler)} does, but
    156      * with no callback handler for password prompting of the keystore.  This will work
    157      * in most cases, provided the client is not required to provide a certificate to
    158      * the server.
    159      *
    160      *
    161      * @param config the connection configuration.
    162      */
    163     public XMPPConnection(ConnectionConfiguration config) {
    164         super(config);
    165     }
    166 
    167     /**
    168      * Creates a new XMPP connection using the specified connection configuration.<p>
    169      * <p/>
    170      * Manually specifying connection configuration information is suitable for
    171      * advanced users of the API. In many cases, using the
    172      * {@link #XMPPConnection(String)} constructor is a better approach.<p>
    173      * <p/>
    174      * Note that XMPPConnection constructors do not establish a connection to the server
    175      * and you must call {@link #connect()}.<p>
    176      * <p/>
    177      *
    178      * The CallbackHandler will only be used if the connection requires the client provide
    179      * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
    180      * to prompt for a password to unlock the keystore containing the SSL certificate.
    181      *
    182      * @param config the connection configuration.
    183      * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
    184      */
    185     public XMPPConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) {
    186         super(config);
    187         config.setCallbackHandler(callbackHandler);
    188     }
    189 
    190     public String getConnectionID() {
    191         if (!isConnected()) {
    192             return null;
    193         }
    194         return connectionID;
    195     }
    196 
    197     public String getUser() {
    198         if (!isAuthenticated()) {
    199             return null;
    200         }
    201         return user;
    202     }
    203 
    204     @Override
    205     public synchronized void login(String username, String password, String resource) throws XMPPException {
    206         if (!isConnected()) {
    207             throw new IllegalStateException("Not connected to server.");
    208         }
    209         if (authenticated) {
    210             throw new IllegalStateException("Already logged in to server.");
    211         }
    212         // Do partial version of nameprep on the username.
    213         username = username.toLowerCase().trim();
    214 
    215         String response;
    216         if (config.isSASLAuthenticationEnabled() &&
    217                 saslAuthentication.hasNonAnonymousAuthentication()) {
    218             // Authenticate using SASL
    219             if (password != null) {
    220                 response = saslAuthentication.authenticate(username, password, resource);
    221             }
    222             else {
    223                 response = saslAuthentication
    224                         .authenticate(username, resource, config.getCallbackHandler());
    225             }
    226         }
    227         else {
    228             // Authenticate using Non-SASL
    229             response = new NonSASLAuthentication(this).authenticate(username, password, resource);
    230         }
    231 
    232         // Set the user.
    233         if (response != null) {
    234             this.user = response;
    235             // Update the serviceName with the one returned by the server
    236             config.setServiceName(StringUtils.parseServer(response));
    237         }
    238         else {
    239             this.user = username + "@" + getServiceName();
    240             if (resource != null) {
    241                 this.user += "/" + resource;
    242             }
    243         }
    244 
    245         // If compression is enabled then request the server to use stream compression
    246         if (config.isCompressionEnabled()) {
    247             useCompression();
    248         }
    249 
    250         // Indicate that we're now authenticated.
    251         authenticated = true;
    252         anonymous = false;
    253 
    254         // Create the roster if it is not a reconnection or roster already created by getRoster()
    255         if (this.roster == null) {
    256         	if(rosterStorage==null){
    257         		this.roster = new Roster(this);
    258         	}
    259         	else{
    260         		this.roster = new Roster(this,rosterStorage);
    261         	}
    262         }
    263         if (config.isRosterLoadedAtLogin()) {
    264             this.roster.reload();
    265         }
    266 
    267         // Set presence to online.
    268         if (config.isSendPresence()) {
    269             packetWriter.sendPacket(new Presence(Presence.Type.available));
    270         }
    271 
    272         // Stores the authentication for future reconnection
    273         config.setLoginInfo(username, password, resource);
    274 
    275         // If debugging is enabled, change the the debug window title to include the
    276         // name we are now logged-in as.
    277         // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
    278         // will be null
    279         if (config.isDebuggerEnabled() && debugger != null) {
    280             debugger.userHasLogged(user);
    281         }
    282     }
    283 
    284     @Override
    285     public synchronized void loginAnonymously() throws XMPPException {
    286         if (!isConnected()) {
    287             throw new IllegalStateException("Not connected to server.");
    288         }
    289         if (authenticated) {
    290             throw new IllegalStateException("Already logged in to server.");
    291         }
    292 
    293         String response;
    294         if (config.isSASLAuthenticationEnabled() &&
    295                 saslAuthentication.hasAnonymousAuthentication()) {
    296             response = saslAuthentication.authenticateAnonymously();
    297         }
    298         else {
    299             // Authenticate using Non-SASL
    300             response = new NonSASLAuthentication(this).authenticateAnonymously();
    301         }
    302 
    303         // Set the user value.
    304         this.user = response;
    305         // Update the serviceName with the one returned by the server
    306         config.setServiceName(StringUtils.parseServer(response));
    307 
    308         // If compression is enabled then request the server to use stream compression
    309         if (config.isCompressionEnabled()) {
    310             useCompression();
    311         }
    312 
    313         // Set presence to online.
    314         packetWriter.sendPacket(new Presence(Presence.Type.available));
    315 
    316         // Indicate that we're now authenticated.
    317         authenticated = true;
    318         anonymous = true;
    319 
    320         // If debugging is enabled, change the the debug window title to include the
    321         // name we are now logged-in as.
    322         // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
    323         // will be null
    324         if (config.isDebuggerEnabled() && debugger != null) {
    325             debugger.userHasLogged(user);
    326         }
    327     }
    328 
    329     public Roster getRoster() {
    330         // synchronize against login()
    331         synchronized(this) {
    332             // if connection is authenticated the roster is already set by login()
    333             // or a previous call to getRoster()
    334             if (!isAuthenticated() || isAnonymous()) {
    335                 if (roster == null) {
    336                     roster = new Roster(this);
    337                 }
    338                 return roster;
    339             }
    340         }
    341 
    342         if (!config.isRosterLoadedAtLogin()) {
    343             roster.reload();
    344         }
    345         // If this is the first time the user has asked for the roster after calling
    346         // login, we want to wait for the server to send back the user's roster. This
    347         // behavior shields API users from having to worry about the fact that roster
    348         // operations are asynchronous, although they'll still have to listen for
    349         // changes to the roster. Note: because of this waiting logic, internal
    350         // Smack code should be wary about calling the getRoster method, and may need to
    351         // access the roster object directly.
    352         if (!roster.rosterInitialized) {
    353             try {
    354                 synchronized (roster) {
    355                     long waitTime = SmackConfiguration.getPacketReplyTimeout();
    356                     long start = System.currentTimeMillis();
    357                     while (!roster.rosterInitialized) {
    358                         if (waitTime <= 0) {
    359                             break;
    360                         }
    361                         roster.wait(waitTime);
    362                         long now = System.currentTimeMillis();
    363                         waitTime -= now - start;
    364                         start = now;
    365                     }
    366                 }
    367             }
    368             catch (InterruptedException ie) {
    369                 // Ignore.
    370             }
    371         }
    372         return roster;
    373     }
    374 
    375     public boolean isConnected() {
    376         return connected;
    377     }
    378 
    379     public boolean isSecureConnection() {
    380         return isUsingTLS();
    381     }
    382 
    383     public boolean isSocketClosed() {
    384         return socketClosed;
    385     }
    386 
    387     public boolean isAuthenticated() {
    388         return authenticated;
    389     }
    390 
    391     public boolean isAnonymous() {
    392         return anonymous;
    393     }
    394 
    395     /**
    396      * Closes the connection by setting presence to unavailable then closing the stream to
    397      * the XMPP server. The shutdown logic will be used during a planned disconnection or when
    398      * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
    399      * packet reader, packet writer, and {@link Roster} will not be removed; thus
    400      * connection's state is kept.
    401      *
    402      * @param unavailablePresence the presence packet to send during shutdown.
    403      */
    404     protected void shutdown(Presence unavailablePresence) {
    405         // Set presence to offline.
    406         if (packetWriter != null) {
    407                 packetWriter.sendPacket(unavailablePresence);
    408         }
    409 
    410         this.setWasAuthenticated(authenticated);
    411         authenticated = false;
    412 
    413         if (packetReader != null) {
    414                 packetReader.shutdown();
    415         }
    416         if (packetWriter != null) {
    417                 packetWriter.shutdown();
    418         }
    419 
    420         // Wait 150 ms for processes to clean-up, then shutdown.
    421         try {
    422             Thread.sleep(150);
    423         }
    424         catch (Exception e) {
    425             // Ignore.
    426         }
    427 
    428 	// Set socketClosed to true. This will cause the PacketReader
    429 	// and PacketWriter to ingore any Exceptions that are thrown
    430 	// because of a read/write from/to a closed stream.
    431 	// It is *important* that this is done before socket.close()!
    432         socketClosed = true;
    433         try {
    434                 socket.close();
    435         } catch (Exception e) {
    436                 e.printStackTrace();
    437         }
    438 	// In most cases the close() should be successful, so set
    439 	// connected to false here.
    440         connected = false;
    441 
    442         // Close down the readers and writers.
    443         if (reader != null) {
    444             try {
    445 		// Should already be closed by the previous
    446 		// socket.close(). But just in case do it explicitly.
    447                 reader.close();
    448             }
    449             catch (Throwable ignore) { /* ignore */ }
    450             reader = null;
    451         }
    452         if (writer != null) {
    453             try {
    454 		// Should already be closed by the previous
    455 		// socket.close(). But just in case do it explicitly.
    456                 writer.close();
    457             }
    458             catch (Throwable ignore) { /* ignore */ }
    459             writer = null;
    460         }
    461 
    462         // Make sure that the socket is really closed
    463         try {
    464 	    // Does nothing if the socket is already closed
    465             socket.close();
    466         }
    467         catch (Exception e) {
    468             // Ignore.
    469         }
    470 
    471         saslAuthentication.init();
    472     }
    473 
    474     public synchronized void disconnect(Presence unavailablePresence) {
    475         // If not connected, ignore this request.
    476         PacketReader packetReader = this.packetReader;
    477         PacketWriter packetWriter = this.packetWriter;
    478         if (packetReader == null || packetWriter == null) {
    479             return;
    480         }
    481 
    482         if (!isConnected()) {
    483             return;
    484         }
    485 
    486         shutdown(unavailablePresence);
    487 
    488         if (roster != null) {
    489             roster.cleanup();
    490             roster = null;
    491         }
    492         chatManager = null;
    493 
    494         wasAuthenticated = false;
    495 
    496         packetWriter.cleanup();
    497         packetReader.cleanup();
    498     }
    499 
    500     public void sendPacket(Packet packet) {
    501         if (!isConnected()) {
    502             throw new IllegalStateException("Not connected to server.");
    503         }
    504         if (packet == null) {
    505             throw new NullPointerException("Packet is null.");
    506         }
    507         packetWriter.sendPacket(packet);
    508     }
    509 
    510     /**
    511      * Registers a packet interceptor with this connection. The interceptor will be
    512      * invoked every time a packet is about to be sent by this connection. Interceptors
    513      * may modify the packet to be sent. A packet filter determines which packets
    514      * will be delivered to the interceptor.
    515      *
    516      * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
    517      * @param packetFilter      the packet filter to use.
    518      * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}.
    519      */
    520     public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
    521             PacketFilter packetFilter) {
    522         addPacketInterceptor(packetInterceptor, packetFilter);
    523     }
    524 
    525     /**
    526      * Removes a packet interceptor.
    527      *
    528      * @param packetInterceptor the packet interceptor to remove.
    529      * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}.
    530      */
    531     public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
    532         removePacketInterceptor(packetInterceptor);
    533     }
    534 
    535     /**
    536      * Registers a packet listener with this connection. The listener will be
    537      * notified of every packet that this connection sends. A packet filter determines
    538      * which packets will be delivered to the listener. Note that the thread
    539      * that writes packets will be used to invoke the listeners. Therefore, each
    540      * packet listener should complete all operations quickly or use a different
    541      * thread for processing.
    542      *
    543      * @param packetListener the packet listener to notify of sent packets.
    544      * @param packetFilter   the packet filter to use.
    545      * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}.
    546      */
    547     public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
    548         addPacketSendingListener(packetListener, packetFilter);
    549     }
    550 
    551     /**
    552      * Removes a packet listener for sending packets from this connection.
    553      *
    554      * @param packetListener the packet listener to remove.
    555      * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}.
    556      */
    557     public void removePacketWriterListener(PacketListener packetListener) {
    558         removePacketSendingListener(packetListener);
    559     }
    560 
    561     private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
    562         XMPPException exception = null;
    563         Iterator<HostAddress> it = config.getHostAddresses().iterator();
    564         List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
    565         boolean xmppIOError = false;
    566         while (it.hasNext()) {
    567             exception = null;
    568             HostAddress hostAddress = it.next();
    569             String host = hostAddress.getFQDN();
    570             int port = hostAddress.getPort();
    571             try {
    572                 if (config.getSocketFactory() == null) {
    573                     this.socket = new Socket(host, port);
    574                 }
    575                 else {
    576                     this.socket = config.getSocketFactory().createSocket(host, port);
    577                 }
    578             } catch (UnknownHostException uhe) {
    579                 String errorMessage = "Could not connect to " + host + ":" + port + ".";
    580                 exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout,
    581                         errorMessage), uhe);
    582             } catch (IOException ioe) {
    583                 String errorMessage = "XMPPError connecting to " + host + ":" + port + ".";
    584                 exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error,
    585                         errorMessage), ioe);
    586                 xmppIOError = true;
    587             }
    588             if (exception == null) {
    589                 // We found a host to connect to, break here
    590                 config.setUsedHostAddress(hostAddress);
    591                 break;
    592             }
    593             hostAddress.setException(exception);
    594             failedAddresses.add(hostAddress);
    595             if (!it.hasNext()) {
    596                 // There are no more host addresses to try
    597                 // throw an exception and report all tried
    598                 // HostAddresses in the exception
    599                 StringBuilder sb = new StringBuilder();
    600                 for (HostAddress fha : failedAddresses) {
    601                     sb.append(fha.getErrorMessage());
    602                     sb.append("; ");
    603                 }
    604                 XMPPError xmppError;
    605                 if (xmppIOError) {
    606                     xmppError = new XMPPError(XMPPError.Condition.remote_server_error);
    607                 }
    608                 else {
    609                     xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout);
    610                 }
    611                 throw new XMPPException(sb.toString(), xmppError);
    612             }
    613         }
    614         socketClosed = false;
    615         initConnection();
    616     }
    617 
    618     /**
    619      * Initializes the connection by creating a packet reader and writer and opening a
    620      * XMPP stream to the server.
    621      *
    622      * @throws XMPPException if establishing a connection to the server fails.
    623      */
    624     private void initConnection() throws XMPPException {
    625         boolean isFirstInitialization = packetReader == null || packetWriter == null;
    626         compressionHandler = null;
    627         serverAckdCompression = false;
    628 
    629         // Set the reader and writer instance variables
    630         initReaderAndWriter();
    631 
    632         try {
    633             if (isFirstInitialization) {
    634                 packetWriter = new PacketWriter(this);
    635                 packetReader = new PacketReader(this);
    636 
    637                 // If debugging is enabled, we should start the thread that will listen for
    638                 // all packets and then log them.
    639                 if (config.isDebuggerEnabled()) {
    640                     addPacketListener(debugger.getReaderListener(), null);
    641                     if (debugger.getWriterListener() != null) {
    642                         addPacketSendingListener(debugger.getWriterListener(), null);
    643                     }
    644                 }
    645             }
    646             else {
    647                 packetWriter.init();
    648                 packetReader.init();
    649             }
    650 
    651             // Start the packet writer. This will open a XMPP stream to the server
    652             packetWriter.startup();
    653             // Start the packet reader. The startup() method will block until we
    654             // get an opening stream packet back from server.
    655             packetReader.startup();
    656 
    657             // Make note of the fact that we're now connected.
    658             connected = true;
    659 
    660             if (isFirstInitialization) {
    661                 // Notify listeners that a new connection has been established
    662                 for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
    663                     listener.connectionCreated(this);
    664                 }
    665             }
    666             else if (!wasAuthenticated) {
    667                 notifyReconnection();
    668             }
    669 
    670         }
    671         catch (XMPPException ex) {
    672             // An exception occurred in setting up the connection. Make sure we shut down the
    673             // readers and writers and close the socket.
    674 
    675             if (packetWriter != null) {
    676                 try {
    677                     packetWriter.shutdown();
    678                 }
    679                 catch (Throwable ignore) { /* ignore */ }
    680                 packetWriter = null;
    681             }
    682             if (packetReader != null) {
    683                 try {
    684                     packetReader.shutdown();
    685                 }
    686                 catch (Throwable ignore) { /* ignore */ }
    687                 packetReader = null;
    688             }
    689             if (reader != null) {
    690                 try {
    691                     reader.close();
    692                 }
    693                 catch (Throwable ignore) { /* ignore */ }
    694                 reader = null;
    695             }
    696             if (writer != null) {
    697                 try {
    698                     writer.close();
    699                 }
    700                 catch (Throwable ignore) {  /* ignore */}
    701                 writer = null;
    702             }
    703             if (socket != null) {
    704                 try {
    705                     socket.close();
    706                 }
    707                 catch (Exception e) { /* ignore */ }
    708                 socket = null;
    709             }
    710             this.setWasAuthenticated(authenticated);
    711             chatManager = null;
    712             authenticated = false;
    713             connected = false;
    714 
    715             throw ex;        // Everything stoppped. Now throw the exception.
    716         }
    717     }
    718 
    719     private void initReaderAndWriter() throws XMPPException {
    720         try {
    721             if (compressionHandler == null) {
    722                 reader =
    723                         new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
    724                 writer = new BufferedWriter(
    725                         new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
    726             }
    727             else {
    728                 try {
    729                     OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
    730                     writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
    731 
    732                     InputStream is = compressionHandler.getInputStream(socket.getInputStream());
    733                     reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
    734                 }
    735                 catch (Exception e) {
    736                     e.printStackTrace();
    737                     compressionHandler = null;
    738                     reader = new BufferedReader(
    739                             new InputStreamReader(socket.getInputStream(), "UTF-8"));
    740                     writer = new BufferedWriter(
    741                             new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
    742                 }
    743             }
    744         }
    745         catch (IOException ioe) {
    746             throw new XMPPException(
    747                     "XMPPError establishing connection with server.",
    748                     new XMPPError(XMPPError.Condition.remote_server_error,
    749                             "XMPPError establishing connection with server."),
    750                     ioe);
    751         }
    752 
    753         // If debugging is enabled, we open a window and write out all network traffic.
    754         initDebugger();
    755     }
    756 
    757     /***********************************************
    758      * TLS code below
    759      **********************************************/
    760 
    761     /**
    762      * Returns true if the connection to the server has successfully negotiated TLS. Once TLS
    763      * has been negotiatied the connection has been secured.
    764      *
    765      * @return true if the connection to the server has successfully negotiated TLS.
    766      */
    767     public boolean isUsingTLS() {
    768         return usingTLS;
    769     }
    770 
    771     /**
    772      * Notification message saying that the server supports TLS so confirm the server that we
    773      * want to secure the connection.
    774      *
    775      * @param required true when the server indicates that TLS is required.
    776      */
    777     void startTLSReceived(boolean required) {
    778         if (required && config.getSecurityMode() ==
    779                 ConnectionConfiguration.SecurityMode.disabled) {
    780             notifyConnectionError(new IllegalStateException(
    781                     "TLS required by server but not allowed by connection configuration"));
    782             return;
    783         }
    784 
    785         if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
    786             // Do not secure the connection using TLS since TLS was disabled
    787             return;
    788         }
    789         try {
    790             writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
    791             writer.flush();
    792         }
    793         catch (IOException e) {
    794             notifyConnectionError(e);
    795         }
    796     }
    797 
    798     /**
    799      * The server has indicated that TLS negotiation can start. We now need to secure the
    800      * existing plain connection and perform a handshake. This method won't return until the
    801      * connection has finished the handshake or an error occured while securing the connection.
    802      *
    803      * @throws Exception if an exception occurs.
    804      */
    805     void proceedTLSReceived() throws Exception {
    806         SSLContext context = this.config.getCustomSSLContext();
    807         KeyStore ks = null;
    808         KeyManager[] kms = null;
    809         PasswordCallback pcb = null;
    810 
    811         if(config.getCallbackHandler() == null) {
    812            ks = null;
    813         } else if (context == null) {
    814             //System.out.println("Keystore type: "+configuration.getKeystoreType());
    815             if(config.getKeystoreType().equals("NONE")) {
    816                 ks = null;
    817                 pcb = null;
    818             }
    819             else if(config.getKeystoreType().equals("PKCS11")) {
    820                 try {
    821                     Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
    822                     String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library();
    823                     ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes());
    824                     Provider p = (Provider)c.newInstance(config);
    825                     Security.addProvider(p);
    826                     ks = KeyStore.getInstance("PKCS11",p);
    827                     pcb = new PasswordCallback("PKCS11 Password: ",false);
    828                     this.config.getCallbackHandler().handle(new Callback[]{pcb});
    829                     ks.load(null,pcb.getPassword());
    830                 }
    831                 catch (Exception e) {
    832                     ks = null;
    833                     pcb = null;
    834                 }
    835             }
    836             else if(config.getKeystoreType().equals("Apple")) {
    837                 ks = KeyStore.getInstance("KeychainStore","Apple");
    838                 ks.load(null,null);
    839                 //pcb = new PasswordCallback("Apple Keychain",false);
    840                 //pcb.setPassword(null);
    841             }
    842             else {
    843                 ks = KeyStore.getInstance(config.getKeystoreType());
    844                 try {
    845                     pcb = new PasswordCallback("Keystore Password: ",false);
    846                     config.getCallbackHandler().handle(new Callback[]{pcb});
    847                     ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword());
    848                 }
    849                 catch(Exception e) {
    850                     ks = null;
    851                     pcb = null;
    852                 }
    853             }
    854             KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    855             try {
    856                 if(pcb == null) {
    857                     kmf.init(ks,null);
    858                 } else {
    859                     kmf.init(ks,pcb.getPassword());
    860                     pcb.clearPassword();
    861                 }
    862                 kms = kmf.getKeyManagers();
    863             } catch (NullPointerException npe) {
    864                 kms = null;
    865             }
    866         }
    867 
    868         // Verify certificate presented by the server
    869         if (context == null) {
    870             context = SSLContext.getInstance("TLS");
    871             context.init(kms, new javax.net.ssl.TrustManager[] { new ServerTrustManager(getServiceName(), config) },
    872                     new java.security.SecureRandom());
    873         }
    874         Socket plain = socket;
    875         // Secure the plain connection
    876         socket = context.getSocketFactory().createSocket(plain,
    877                 plain.getInetAddress().getHostAddress(), plain.getPort(), true);
    878         socket.setSoTimeout(0);
    879         socket.setKeepAlive(true);
    880         // Initialize the reader and writer with the new secured version
    881         initReaderAndWriter();
    882         // Proceed to do the handshake
    883         ((SSLSocket) socket).startHandshake();
    884         //if (((SSLSocket) socket).getWantClientAuth()) {
    885         //    System.err.println("Connection wants client auth");
    886         //}
    887         //else if (((SSLSocket) socket).getNeedClientAuth()) {
    888         //    System.err.println("Connection needs client auth");
    889         //}
    890         //else {
    891         //    System.err.println("Connection does not require client auth");
    892        // }
    893         // Set that TLS was successful
    894         usingTLS = true;
    895 
    896         // Set the new  writer to use
    897         packetWriter.setWriter(writer);
    898         // Send a new opening stream to the server
    899         packetWriter.openStream();
    900     }
    901 
    902     /**
    903      * Sets the available stream compression methods offered by the server.
    904      *
    905      * @param methods compression methods offered by the server.
    906      */
    907     void setAvailableCompressionMethods(Collection<String> methods) {
    908         compressionMethods = methods;
    909     }
    910 
    911     /**
    912      * Returns the compression handler that can be used for one compression methods offered by the server.
    913      *
    914      * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
    915      *
    916      */
    917     private XMPPInputOutputStream maybeGetCompressionHandler() {
    918         if (compressionMethods != null) {
    919             for (XMPPInputOutputStream handler : compressionHandlers) {
    920                 if (!handler.isSupported())
    921                     continue;
    922 
    923                 String method = handler.getCompressionMethod();
    924                 if (compressionMethods.contains(method))
    925                     return handler;
    926             }
    927         }
    928         return null;
    929     }
    930 
    931     public boolean isUsingCompression() {
    932         return compressionHandler != null && serverAckdCompression;
    933     }
    934 
    935     /**
    936      * Starts using stream compression that will compress network traffic. Traffic can be
    937      * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
    938      * connection. However, the server and the client will need to use more CPU time in order to
    939      * un/compress network data so under high load the server performance might be affected.<p>
    940      * <p/>
    941      * Stream compression has to have been previously offered by the server. Currently only the
    942      * zlib method is supported by the client. Stream compression negotiation has to be done
    943      * before authentication took place.<p>
    944      * <p/>
    945      * Note: to use stream compression the smackx.jar file has to be present in the classpath.
    946      *
    947      * @return true if stream compression negotiation was successful.
    948      */
    949     private boolean useCompression() {
    950         // If stream compression was offered by the server and we want to use
    951         // compression then send compression request to the server
    952         if (authenticated) {
    953             throw new IllegalStateException("Compression should be negotiated before authentication.");
    954         }
    955 
    956         if ((compressionHandler = maybeGetCompressionHandler()) != null) {
    957             requestStreamCompression(compressionHandler.getCompressionMethod());
    958             // Wait until compression is being used or a timeout happened
    959             synchronized (this) {
    960                 try {
    961                     this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
    962                 }
    963                 catch (InterruptedException e) {
    964                     // Ignore.
    965                 }
    966             }
    967             return isUsingCompression();
    968         }
    969         return false;
    970     }
    971 
    972     /**
    973      * Request the server that we want to start using stream compression. When using TLS
    974      * then negotiation of stream compression can only happen after TLS was negotiated. If TLS
    975      * compression is being used the stream compression should not be used.
    976      */
    977     private void requestStreamCompression(String method) {
    978         try {
    979             writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
    980             writer.write("<method>" + method + "</method></compress>");
    981             writer.flush();
    982         }
    983         catch (IOException e) {
    984             notifyConnectionError(e);
    985         }
    986     }
    987 
    988     /**
    989      * Start using stream compression since the server has acknowledged stream compression.
    990      *
    991      * @throws Exception if there is an exception starting stream compression.
    992      */
    993     void startStreamCompression() throws Exception {
    994         serverAckdCompression = true;
    995         // Initialize the reader and writer with the new secured version
    996         initReaderAndWriter();
    997 
    998         // Set the new  writer to use
    999         packetWriter.setWriter(writer);
   1000         // Send a new opening stream to the server
   1001         packetWriter.openStream();
   1002         // Notify that compression is being used
   1003         synchronized (this) {
   1004             this.notify();
   1005         }
   1006     }
   1007 
   1008     /**
   1009      * Notifies the XMPP connection that stream compression was denied so that
   1010      * the connection process can proceed.
   1011      */
   1012     void streamCompressionDenied() {
   1013         synchronized (this) {
   1014             this.notify();
   1015         }
   1016     }
   1017 
   1018     /**
   1019      * Establishes a connection to the XMPP server and performs an automatic login
   1020      * only if the previous connection state was logged (authenticated). It basically
   1021      * creates and maintains a socket connection to the server.<p>
   1022      * <p/>
   1023      * Listeners will be preserved from a previous connection if the reconnection
   1024      * occurs after an abrupt termination.
   1025      *
   1026      * @throws XMPPException if an error occurs while trying to establish the connection.
   1027      *      Two possible errors can occur which will be wrapped by an XMPPException --
   1028      *      UnknownHostException (XMPP error code 504), and IOException (XMPP error code
   1029      *      502). The error codes and wrapped exceptions can be used to present more
   1030      *      appropriate error messages to end-users.
   1031      */
   1032     public void connect() throws XMPPException {
   1033         // Establishes the connection, readers and writers
   1034         connectUsingConfiguration(config);
   1035         // Automatically makes the login if the user was previously connected successfully
   1036         // to the server and the connection was terminated abruptly
   1037         if (connected && wasAuthenticated) {
   1038             // Make the login
   1039             if (isAnonymous()) {
   1040                 // Make the anonymous login
   1041                 loginAnonymously();
   1042             }
   1043             else {
   1044                 login(config.getUsername(), config.getPassword(), config.getResource());
   1045             }
   1046             notifyReconnection();
   1047         }
   1048     }
   1049 
   1050     /**
   1051      * Sets whether the connection has already logged in the server.
   1052      *
   1053      * @param wasAuthenticated true if the connection has already been authenticated.
   1054      */
   1055     private void setWasAuthenticated(boolean wasAuthenticated) {
   1056         if (!this.wasAuthenticated) {
   1057             this.wasAuthenticated = wasAuthenticated;
   1058         }
   1059     }
   1060 
   1061 	@Override
   1062 	public void setRosterStorage(RosterStorage storage)
   1063 			throws IllegalStateException {
   1064 		if(roster!=null){
   1065 			throw new IllegalStateException("Roster is already initialized");
   1066 		}
   1067 		this.rosterStorage = storage;
   1068 	}
   1069 
   1070     /**
   1071      * Sends out a notification that there was an error with the connection
   1072      * and closes the connection. Also prints the stack trace of the given exception
   1073      *
   1074      * @param e the exception that causes the connection close event.
   1075      */
   1076     synchronized void notifyConnectionError(Exception e) {
   1077         // Listeners were already notified of the exception, return right here.
   1078         if (packetReader.done && packetWriter.done) return;
   1079 
   1080         packetReader.done = true;
   1081         packetWriter.done = true;
   1082         // Closes the connection temporary. A reconnection is possible
   1083         shutdown(new Presence(Presence.Type.unavailable));
   1084         // Print the stack trace to help catch the problem
   1085         e.printStackTrace();
   1086         // Notify connection listeners of the error.
   1087         for (ConnectionListener listener : getConnectionListeners()) {
   1088             try {
   1089                 listener.connectionClosedOnError(e);
   1090             }
   1091             catch (Exception e2) {
   1092                 // Catch and print any exception so we can recover
   1093                 // from a faulty listener
   1094                 e2.printStackTrace();
   1095             }
   1096         }
   1097     }
   1098 
   1099 
   1100     /**
   1101      * Sends a notification indicating that the connection was reconnected successfully.
   1102      */
   1103     protected void notifyReconnection() {
   1104         // Notify connection listeners of the reconnection.
   1105         for (ConnectionListener listener : getConnectionListeners()) {
   1106             try {
   1107                 listener.reconnectionSuccessful();
   1108             }
   1109             catch (Exception e) {
   1110                 // Catch and print any exception so we can recover
   1111                 // from a faulty listener
   1112                 e.printStackTrace();
   1113             }
   1114         }
   1115     }
   1116 }
   1117