001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.tcp;
018
019import org.jivesoftware.smack.ConnectionConfiguration;
020import org.jivesoftware.smack.ConnectionCreationListener;
021import org.jivesoftware.smack.ConnectionListener;
022import org.jivesoftware.smack.SASLAuthentication;
023import org.jivesoftware.smack.SmackConfiguration;
024import org.jivesoftware.smack.SmackException;
025import org.jivesoftware.smack.SmackException.AlreadyLoggedInException;
026import org.jivesoftware.smack.SmackException.NoResponseException;
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.SmackException.ConnectionException;
029import org.jivesoftware.smack.XMPPConnection;
030import org.jivesoftware.smack.XMPPException;
031import org.jivesoftware.smack.compression.XMPPInputOutputStream;
032import org.jivesoftware.smack.packet.Packet;
033import org.jivesoftware.smack.packet.Presence;
034import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
035import org.jivesoftware.smack.util.StringUtils;
036import org.jivesoftware.smack.util.dns.HostAddress;
037
038import javax.net.ssl.HostnameVerifier;
039import javax.net.ssl.KeyManager;
040import javax.net.ssl.KeyManagerFactory;
041import javax.net.ssl.SSLContext;
042import javax.net.ssl.SSLSocket;
043import javax.security.auth.callback.Callback;
044import javax.security.auth.callback.CallbackHandler;
045import javax.security.auth.callback.PasswordCallback;
046import javax.security.sasl.SaslException;
047
048import java.io.BufferedReader;
049import java.io.BufferedWriter;
050import java.io.ByteArrayInputStream;
051import java.io.FileInputStream;
052import java.io.IOException;
053import java.io.InputStream;
054import java.io.InputStreamReader;
055import java.io.OutputStream;
056import java.io.OutputStreamWriter;
057import java.io.Reader;
058import java.io.UnsupportedEncodingException;
059import java.io.Writer;
060import java.lang.reflect.Constructor;
061import java.net.Socket;
062import java.security.KeyStore;
063import java.security.Provider;
064import java.security.Security;
065import java.security.cert.CertificateException;
066import java.util.Collection;
067import java.util.Iterator;
068import java.util.LinkedList;
069import java.util.List;
070import java.util.Locale;
071import java.util.logging.Level;
072import java.util.logging.Logger;
073
074/**
075 * Creates a socket connection to a XMPP server. This is the default connection
076 * to a Jabber server and is specified in the XMPP Core (RFC 3920).
077 * 
078 * @see XMPPConnection
079 * @author Matt Tucker
080 */
081public class XMPPTCPConnection extends XMPPConnection {
082
083    private static final Logger LOGGER = Logger.getLogger(XMPPTCPConnection.class.getName());
084
085    /**
086     * The socket which is used for this connection.
087     */
088    Socket socket;
089
090    String connectionID = null;
091    private String user = null;
092    private boolean connected = false;
093    // socketClosed is used concurrent
094    // by XMPPTCPConnection, PacketReader, PacketWriter
095    private volatile boolean socketClosed = false;
096
097    private boolean anonymous = false;
098    private boolean usingTLS = false;
099
100    private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback();
101
102    PacketWriter packetWriter;
103    PacketReader packetReader;
104
105    /**
106     * Collection of available stream compression methods offered by the server.
107     */
108    private Collection<String> compressionMethods;
109
110    /**
111     * Set to true by packet writer if the server acknowledged the compression
112     */
113    private boolean serverAckdCompression = false;
114
115    /**
116     * Lock for the wait()/notify() pattern for the compression negotiation
117     */
118    private final Object compressionLock = new Object();
119
120    /**
121     * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
122     * performed to determine the IP address and port corresponding to the
123     * service name; if that lookup fails, it's assumed that server resides at
124     * <tt>serviceName</tt> with the default port of 5222. Encrypted connections (TLS)
125     * will be used if available, stream compression is disabled, and standard SASL
126     * mechanisms will be used for authentication.<p>
127     * <p/>
128     * This is the simplest constructor for connecting to an XMPP server. Alternatively,
129     * you can get fine-grained control over connection settings using the
130     * {@link #XMPPTCPConnection(ConnectionConfiguration)} constructor.<p>
131     * <p/>
132     * Note that XMPPTCPConnection constructors do not establish a connection to the server
133     * and you must call {@link #connect()}.<p>
134     * <p/>
135     * The CallbackHandler will only be used if the connection requires the client provide
136     * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
137     * to prompt for a password to unlock the keystore containing the SSL certificate.
138     *
139     * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
140     * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
141     */
142    public XMPPTCPConnection(String serviceName, CallbackHandler callbackHandler) {
143        // Create the configuration for this new connection
144        super(new ConnectionConfiguration(serviceName));
145        config.setCallbackHandler(callbackHandler);
146    }
147
148    /**
149     * Creates a new XMPP connection in the same way {@link #XMPPTCPConnection(String,CallbackHandler)} does, but
150     * with no callback handler for password prompting of the keystore.  This will work
151     * in most cases, provided the client is not required to provide a certificate to 
152     * the server.
153     *
154     * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
155     */
156    public XMPPTCPConnection(String serviceName) {
157        // Create the configuration for this new connection
158        super(new ConnectionConfiguration(serviceName));
159    }
160
161    /**
162     * Creates a new XMPP connection in the same way {@link #XMPPTCPConnection(ConnectionConfiguration,CallbackHandler)} does, but
163     * with no callback handler for password prompting of the keystore.  This will work
164     * in most cases, provided the client is not required to provide a certificate to 
165     * the server.
166     *
167     *
168     * @param config the connection configuration.
169     */
170    public XMPPTCPConnection(ConnectionConfiguration config) {
171        super(config);
172    }
173
174    /**
175     * Creates a new XMPP connection using the specified connection configuration.<p>
176     * <p/>
177     * Manually specifying connection configuration information is suitable for
178     * advanced users of the API. In many cases, using the
179     * {@link #XMPPTCPConnection(String)} constructor is a better approach.<p>
180     * <p/>
181     * Note that XMPPTCPConnection constructors do not establish a connection to the server
182     * and you must call {@link #connect()}.<p>
183     * <p/>
184     *
185     * The CallbackHandler will only be used if the connection requires the client provide
186     * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
187     * to prompt for a password to unlock the keystore containing the SSL certificate.
188     *
189     * @param config the connection configuration.
190     * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
191     */
192    public XMPPTCPConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) {
193        super(config);
194        config.setCallbackHandler(callbackHandler);
195    }
196
197    public String getConnectionID() {
198        if (!isConnected()) {
199            return null;
200        }
201        return connectionID;
202    }
203
204    public String getUser() {
205        if (!isAuthenticated()) {
206            return null;
207        }
208        return user;
209    }
210
211    /**
212     * Install a parsing exception callback, which will be invoked once an exception is encountered while parsing a
213     * stanza
214     * 
215     * @param callback the callback to install
216     */
217    public void setParsingExceptionCallback(ParsingExceptionCallback callback) {
218        parsingExceptionCallback = callback;
219    }
220
221    /**
222     * Get the current active parsing exception callback.
223     *  
224     * @return the active exception callback or null if there is none
225     */
226    public ParsingExceptionCallback getParsingExceptionCallback() {
227        return parsingExceptionCallback;
228    }
229
230    @Override
231    public synchronized void login(String username, String password, String resource) throws XMPPException, SmackException, SaslException, IOException {
232        if (!isConnected()) {
233            throw new NotConnectedException();
234        }
235        if (authenticated) {
236            throw new AlreadyLoggedInException();
237        }
238        // Do partial version of nameprep on the username.
239        username = username.toLowerCase(Locale.US).trim();
240
241        if (saslAuthentication.hasNonAnonymousAuthentication()) {
242            // Authenticate using SASL
243            if (password != null) {
244                saslAuthentication.authenticate(username, password, resource);
245            }
246            else {
247                saslAuthentication.authenticate(resource, config.getCallbackHandler());
248            }
249        } else {
250            throw new SaslException("No non-anonymous SASL authentication mechanism available");
251        }
252
253        // If compression is enabled then request the server to use stream compression. XEP-170
254        // recommends to perform stream compression before resource binding.
255        if (config.isCompressionEnabled()) {
256            useCompression();
257        }
258
259        // Set the user.
260        String response = bindResourceAndEstablishSession(resource);
261        if (response != null) {
262            this.user = response;
263            // Update the serviceName with the one returned by the server
264            setServiceName(StringUtils.parseServer(response));
265        }
266        else {
267            this.user = username + "@" + getServiceName();
268            if (resource != null) {
269                this.user += "/" + resource;
270            }
271        }
272
273        // Indicate that we're now authenticated.
274        authenticated = true;
275        anonymous = false;
276
277        // Set presence to online.
278        if (config.isSendPresence()) {
279            sendPacket(new Presence(Presence.Type.available));
280        }
281
282        // Stores the authentication for future reconnection
283        setLoginInfo(username, password, resource);
284
285        // If debugging is enabled, change the the debug window title to include the
286        // name we are now logged-in as.
287        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
288        // will be null
289        if (config.isDebuggerEnabled() && debugger != null) {
290            debugger.userHasLogged(user);
291        }
292        callConnectionAuthenticatedListener();
293    }
294
295    @Override
296    public synchronized void loginAnonymously() throws XMPPException, SmackException, SaslException, IOException {
297        if (!isConnected()) {
298            throw new NotConnectedException();
299        }
300        if (authenticated) {
301            throw new AlreadyLoggedInException();
302        }
303
304        if (saslAuthentication.hasAnonymousAuthentication()) {
305            saslAuthentication.authenticateAnonymously();
306        }
307        else {
308            throw new SaslException("No anonymous SASL authentication mechanism available");
309        }
310
311        String response = bindResourceAndEstablishSession(null);
312        // Set the user value.
313        this.user = response;
314        // Update the serviceName with the one returned by the server
315        setServiceName(StringUtils.parseServer(response));
316
317        // If compression is enabled then request the server to use stream compression
318        if (config.isCompressionEnabled()) {
319            useCompression();
320        }
321
322        // Set presence to online.
323        sendPacket(new Presence(Presence.Type.available));
324
325        // Indicate that we're now authenticated.
326        authenticated = true;
327        anonymous = true;
328
329        // If debugging is enabled, change the the debug window title to include the
330        // name we are now logged-in as.
331        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
332        // will be null
333        if (config.isDebuggerEnabled() && debugger != null) {
334            debugger.userHasLogged(user);
335        }
336        callConnectionAuthenticatedListener();
337    }
338
339    public boolean isConnected() {
340        return connected;
341    }
342
343    public boolean isSecureConnection() {
344        return isUsingTLS();
345    }
346
347    public boolean isSocketClosed() {
348        return socketClosed;
349    }
350
351    public boolean isAuthenticated() {
352        return authenticated;
353    }
354
355    public boolean isAnonymous() {
356        return anonymous;
357    }
358
359    /**
360     * Shuts the current connection down. After this method returns, the connection must be ready
361     * for re-use by connect.
362     */
363    @Override
364    protected void shutdown() {
365        if (packetReader != null) {
366                packetReader.shutdown();
367        }
368        if (packetWriter != null) {
369                packetWriter.shutdown();
370        }
371
372        // Set socketClosed to true. This will cause the PacketReader
373        // and PacketWriter to ignore any Exceptions that are thrown
374        // because of a read/write from/to a closed stream.
375        // It is *important* that this is done before socket.close()!
376        socketClosed = true;
377        try {
378                socket.close();
379        } catch (Exception e) {
380                LOGGER.log(Level.WARNING, "shutdown", e);
381        }
382
383        setWasAuthenticated(authenticated);
384        authenticated = false;
385        connected = false;
386        usingTLS = false;
387        reader = null;
388        writer = null;
389    }
390
391    @Override
392    protected void sendPacketInternal(Packet packet) throws NotConnectedException {
393        packetWriter.sendPacket(packet);
394    }
395
396    private void connectUsingConfiguration(ConnectionConfiguration config) throws SmackException, IOException {
397        Exception exception = null;
398        try {
399            maybeResolveDns();
400        }
401        catch (Exception e) {
402            throw new SmackException(e);
403        }
404        Iterator<HostAddress> it = config.getHostAddresses().iterator();
405        List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
406        while (it.hasNext()) {
407            exception = null;
408            HostAddress hostAddress = it.next();
409            String host = hostAddress.getFQDN();
410            int port = hostAddress.getPort();
411            try {
412                if (config.getSocketFactory() == null) {
413                    this.socket = new Socket(host, port);
414                }
415                else {
416                    this.socket = config.getSocketFactory().createSocket(host, port);
417                }
418            } catch (Exception e) {
419                exception = e;
420            }
421            if (exception == null) {
422                // We found a host to connect to, break here
423                host = hostAddress.getFQDN();
424                port = hostAddress.getPort();
425                break;
426            }
427            hostAddress.setException(exception);
428            failedAddresses.add(hostAddress);
429            if (!it.hasNext()) {
430                // There are no more host addresses to try
431                // throw an exception and report all tried
432                // HostAddresses in the exception
433                throw new ConnectionException(failedAddresses);
434            }
435        }
436        socketClosed = false;
437        initConnection();
438    }
439
440    /**
441     * Initializes the connection by creating a packet reader and writer and opening a
442     * XMPP stream to the server.
443     *
444     * @throws XMPPException if establishing a connection to the server fails.
445     * @throws SmackException if the server failes to respond back or if there is anther error.
446     * @throws IOException 
447     */
448    private void initConnection() throws SmackException, IOException {
449        boolean isFirstInitialization = packetReader == null || packetWriter == null;
450        compressionHandler = null;
451        serverAckdCompression = false;
452
453        // Set the reader and writer instance variables
454        initReaderAndWriter();
455
456        try {
457            if (isFirstInitialization) {
458                packetWriter = new PacketWriter(this);
459                packetReader = new PacketReader(this);
460
461                // If debugging is enabled, we should start the thread that will listen for
462                // all packets and then log them.
463                if (config.isDebuggerEnabled()) {
464                    addPacketListener(debugger.getReaderListener(), null);
465                    if (debugger.getWriterListener() != null) {
466                        addPacketSendingListener(debugger.getWriterListener(), null);
467                    }
468                }
469            }
470            else {
471                packetWriter.init();
472                packetReader.init();
473            }
474
475            // Start the packet writer. This will open a XMPP stream to the server
476            packetWriter.startup();
477            // Start the packet reader. The startup() method will block until we
478            // get an opening stream packet back from server.
479            packetReader.startup();
480
481            // Make note of the fact that we're now connected.
482            connected = true;
483
484            if (isFirstInitialization) {
485                // Notify listeners that a new connection has been established
486                for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
487                    listener.connectionCreated(this);
488                }
489            }
490
491        }
492        catch (SmackException ex) {
493            // An exception occurred in setting up the connection.
494            shutdown();
495            // Everything stoppped. Now throw the exception.
496            throw ex;
497        }
498    }
499
500    private void initReaderAndWriter() throws IOException {
501        try {
502            if (compressionHandler == null) {
503                reader =
504                        new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
505                writer = new BufferedWriter(
506                        new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
507            }
508            else {
509                try {
510                    OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
511                    writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
512
513                    InputStream is = compressionHandler.getInputStream(socket.getInputStream());
514                    reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
515                }
516                catch (Exception e) {
517                    LOGGER.log(Level.WARNING, "initReaderAndWriter()", e);
518                    compressionHandler = null;
519                    reader = new BufferedReader(
520                            new InputStreamReader(socket.getInputStream(), "UTF-8"));
521                    writer = new BufferedWriter(
522                            new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
523                }
524            }
525        }
526        catch (UnsupportedEncodingException ioe) {
527            throw new IllegalStateException(ioe);
528        }
529
530        // If debugging is enabled, we open a window and write out all network traffic.
531        initDebugger();
532    }
533
534    /***********************************************
535     * TLS code below
536     **********************************************/
537
538    /**
539     * Returns true if the connection to the server has successfully negotiated TLS. Once TLS
540     * has been negotiatied the connection has been secured.
541     *
542     * @return true if the connection to the server has successfully negotiated TLS.
543     */
544    public boolean isUsingTLS() {
545        return usingTLS;
546    }
547
548    /**
549     * Notification message saying that the server supports TLS so confirm the server that we
550     * want to secure the connection.
551     *
552     * @param required true when the server indicates that TLS is required.
553     * @throws IOException if an exception occurs.
554     */
555    void startTLSReceived(boolean required) throws IOException {
556        if (required && config.getSecurityMode() ==
557                ConnectionConfiguration.SecurityMode.disabled) {
558            notifyConnectionError(new IllegalStateException(
559                    "TLS required by server but not allowed by connection configuration"));
560            return;
561        }
562
563        if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
564            // Do not secure the connection using TLS since TLS was disabled
565            return;
566        }
567        writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
568        writer.flush();
569    }
570
571    /**
572     * The server has indicated that TLS negotiation can start. We now need to secure the
573     * existing plain connection and perform a handshake. This method won't return until the
574     * connection has finished the handshake or an error occurred while securing the connection.
575     *
576     * @throws Exception if an exception occurs.
577     */
578    void proceedTLSReceived() throws Exception {
579        SSLContext context = this.config.getCustomSSLContext();
580        KeyStore ks = null;
581        KeyManager[] kms = null;
582        PasswordCallback pcb = null;
583
584        if(config.getCallbackHandler() == null) {
585           ks = null;
586        } else if (context == null) {
587            if(config.getKeystoreType().equals("NONE")) {
588                ks = null;
589                pcb = null;
590            }
591            else if(config.getKeystoreType().equals("PKCS11")) {
592                try {
593                    Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
594                    String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library();
595                    ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes());
596                    Provider p = (Provider)c.newInstance(config);
597                    Security.addProvider(p);
598                    ks = KeyStore.getInstance("PKCS11",p);
599                    pcb = new PasswordCallback("PKCS11 Password: ",false);
600                    this.config.getCallbackHandler().handle(new Callback[]{pcb});
601                    ks.load(null,pcb.getPassword());
602                }
603                catch (Exception e) {
604                    ks = null;
605                    pcb = null;
606                }
607            }
608            else if(config.getKeystoreType().equals("Apple")) {
609                ks = KeyStore.getInstance("KeychainStore","Apple");
610                ks.load(null,null);
611                //pcb = new PasswordCallback("Apple Keychain",false);
612                //pcb.setPassword(null);
613            }
614            else {
615                ks = KeyStore.getInstance(config.getKeystoreType());
616                try {
617                    pcb = new PasswordCallback("Keystore Password: ",false);
618                    config.getCallbackHandler().handle(new Callback[]{pcb});
619                    ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword());
620                }
621                catch(Exception e) {
622                    ks = null;
623                    pcb = null;
624                }
625            }
626            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
627            try {
628                if(pcb == null) {
629                    kmf.init(ks,null);
630                } else {
631                    kmf.init(ks,pcb.getPassword());
632                    pcb.clearPassword();
633                }
634                kms = kmf.getKeyManagers();
635            } catch (NullPointerException npe) {
636                kms = null;
637            }
638        }
639
640        // If the user didn't specify a SSLContext, use the default one
641        if (context == null) {
642            context = SSLContext.getInstance("TLS");
643            context.init(kms, null, new java.security.SecureRandom());
644        }
645        Socket plain = socket;
646        // Secure the plain connection
647        socket = context.getSocketFactory().createSocket(plain,
648                plain.getInetAddress().getHostAddress(), plain.getPort(), true);
649        // Initialize the reader and writer with the new secured version
650        initReaderAndWriter();
651
652        final SSLSocket sslSocket = (SSLSocket) socket;
653        try {
654            // Proceed to do the handshake
655            sslSocket.startHandshake();
656        }
657        catch (IOException e) {
658            setConnectionException(e);
659            throw e;
660        }
661
662        final HostnameVerifier verifier = getConfiguration().getHostnameVerifier();
663        if (verifier != null && !verifier.verify(getServiceName(), sslSocket.getSession())) {
664            throw new CertificateException("Hostname verification of certificate failed. Certificate does not authenticate " + getServiceName());
665        }
666
667        //if (((SSLSocket) socket).getWantClientAuth()) {
668        //    System.err.println("XMPPConnection wants client auth");
669        //}
670        //else if (((SSLSocket) socket).getNeedClientAuth()) {
671        //    System.err.println("XMPPConnection needs client auth");
672        //}
673        //else {
674        //    System.err.println("XMPPConnection does not require client auth");
675       // }
676        // Set that TLS was successful
677        usingTLS = true;
678
679        // Set the new  writer to use
680        packetWriter.setWriter(writer);
681        // Send a new opening stream to the server
682        packetWriter.openStream();
683    }
684
685    /**
686     * Sets the available stream compression methods offered by the server.
687     *
688     * @param methods compression methods offered by the server.
689     */
690    void setAvailableCompressionMethods(Collection<String> methods) {
691        compressionMethods = methods;
692    }
693
694    /**
695     * Returns the compression handler that can be used for one compression methods offered by the server.
696     * 
697     * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
698     * 
699     */
700    private XMPPInputOutputStream maybeGetCompressionHandler() {
701        if (compressionMethods != null) {
702            for (XMPPInputOutputStream handler : SmackConfiguration.getCompresionHandlers()) {
703                String method = handler.getCompressionMethod();
704                if (compressionMethods.contains(method))
705                    return handler;
706            }
707        }
708        return null;
709    }
710
711    public boolean isUsingCompression() {
712        return compressionHandler != null && serverAckdCompression;
713    }
714
715    /**
716     * Starts using stream compression that will compress network traffic. Traffic can be
717     * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
718     * connection. However, the server and the client will need to use more CPU time in order to
719     * un/compress network data so under high load the server performance might be affected.
720     * <p>
721     * <p>
722     * Stream compression has to have been previously offered by the server. Currently only the
723     * zlib method is supported by the client. Stream compression negotiation has to be done
724     * before authentication took place.<p>
725     * <p>
726     *
727     * @return true if stream compression negotiation was successful.
728     * @throws IOException if the compress stanza could not be send
729     */
730    private boolean useCompression() throws IOException {
731        // If stream compression was offered by the server and we want to use
732        // compression then send compression request to the server
733        if (authenticated) {
734            throw new IllegalStateException("Compression should be negotiated before authentication.");
735        }
736
737        if ((compressionHandler = maybeGetCompressionHandler()) != null) {
738            synchronized (compressionLock) {
739                requestStreamCompression(compressionHandler.getCompressionMethod());
740                // Wait until compression is being used or a timeout happened
741                try {
742                    compressionLock.wait(getPacketReplyTimeout());
743                }
744                catch (InterruptedException e) {
745                    // Ignore.
746                }
747            }
748            return isUsingCompression();
749        }
750        return false;
751    }
752
753    /**
754     * Request the server that we want to start using stream compression. When using TLS
755     * then negotiation of stream compression can only happen after TLS was negotiated. If TLS
756     * compression is being used the stream compression should not be used.
757     * @throws IOException if the compress stanza could not be send
758     */
759    private void requestStreamCompression(String method) throws IOException {
760        writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
761        writer.write("<method>" + method + "</method></compress>");
762        writer.flush();
763    }
764
765    /**
766     * Start using stream compression since the server has acknowledged stream compression.
767     *
768     * @throws IOException if there is an exception starting stream compression.
769     */
770    void startStreamCompression() throws IOException {
771        serverAckdCompression = true;
772        // Initialize the reader and writer with the new secured version
773        initReaderAndWriter();
774
775        // Set the new  writer to use
776        packetWriter.setWriter(writer);
777        // Send a new opening stream to the server
778        packetWriter.openStream();
779        // Notify that compression is being used
780        streamCompressionNegotiationDone();
781    }
782
783    /**
784     * Notifies the XMPP connection that stream compression negotiation is done so that the
785     * connection process can proceed.
786     */
787    void streamCompressionNegotiationDone() {
788        synchronized (compressionLock) {
789            compressionLock.notify();
790        }
791    }
792
793    /**
794     * Establishes a connection to the XMPP server and performs an automatic login
795     * only if the previous connection state was logged (authenticated). It basically
796     * creates and maintains a socket connection to the server.<p>
797     * <p/>
798     * Listeners will be preserved from a previous connection if the reconnection
799     * occurs after an abrupt termination.
800     *
801     * @throws XMPPException if an error occurs while trying to establish the connection.
802     * @throws SmackException 
803     * @throws IOException 
804     */
805    @Override
806    protected void connectInternal() throws SmackException, IOException, XMPPException {
807        // Establishes the connection, readers and writers
808        connectUsingConfiguration(config);
809        // TODO is there a case where connectUsing.. does not throw an exception but connected is
810        // still false?
811        if (connected) {
812            callConnectionConnectedListener();
813        }
814        // Automatically makes the login if the user was previously connected successfully
815        // to the server and the connection was terminated abruptly
816        if (connected && wasAuthenticated) {
817            // Make the login
818            if (isAnonymous()) {
819                // Make the anonymous login
820                loginAnonymously();
821            }
822            else {
823                login(config.getUsername(), config.getPassword(), config.getResource());
824            }
825            notifyReconnection();
826        }
827    }
828
829    /**
830     * Sends out a notification that there was an error with the connection
831     * and closes the connection. Also prints the stack trace of the given exception
832     *
833     * @param e the exception that causes the connection close event.
834     */
835    synchronized void notifyConnectionError(Exception e) {
836        // Listeners were already notified of the exception, return right here.
837        if ((packetReader == null || packetReader.done) &&
838                (packetWriter == null || packetWriter.done)) return;
839
840        // Closes the connection temporary. A reconnection is possible
841        shutdown();
842
843        // Notify connection listeners of the error.
844        callConnectionClosedOnErrorListener(e);
845    }
846
847    @Override
848    protected void processPacket(Packet packet) {
849        super.processPacket(packet);
850    }
851
852    @Override
853    protected Reader getReader() {
854        return super.getReader();
855    }
856
857    @Override
858    protected Writer getWriter() {
859        return super.getWriter();
860    }
861
862    @Override
863    protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException {
864        super.throwConnectionExceptionOrNoResponse();
865    }
866
867    @Override
868    protected void setServiceName(String serviceName) {
869        super.setServiceName(serviceName);
870    }
871
872    @Override
873    protected void serverRequiresBinding() {
874        super.serverRequiresBinding();
875    }
876
877    @Override
878    protected void setServiceCapsNode(String node) {
879        super.setServiceCapsNode(node);
880    }
881
882    @Override
883    protected void serverSupportsSession() {
884        super.serverSupportsSession();
885    }
886
887    @Override
888    protected void setRosterVersioningSupported() {
889        super.setRosterVersioningSupported();
890    }
891
892    @Override
893    protected void serverSupportsAccountCreation() {
894        super.serverSupportsAccountCreation();
895    }
896
897    @Override
898    protected SASLAuthentication getSASLAuthentication() {
899        return super.getSASLAuthentication();
900    }
901
902    @Override
903    protected ConnectionConfiguration getConfiguration() {
904        return super.getConfiguration();
905    }
906
907    /**
908     * Sends a notification indicating that the connection was reconnected successfully.
909     */
910    private void notifyReconnection() {
911        // Notify connection listeners of the reconnection.
912        for (ConnectionListener listener : getConnectionListeners()) {
913            try {
914                listener.reconnectionSuccessful();
915            }
916            catch (Exception e) {
917                // Catch and print any exception so we can recover
918                // from a faulty listener
919                LOGGER.log(Level.WARNING, "notifyReconnection()", e);
920            }
921        }
922    }
923}