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        // Stores the authentication for future reconnection
278        setLoginInfo(username, password, resource);
279
280        // If debugging is enabled, change the the debug window title to include the
281        // name we are now logged-in as.
282        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
283        // will be null
284        if (config.isDebuggerEnabled() && debugger != null) {
285            debugger.userHasLogged(user);
286        }
287        callConnectionAuthenticatedListener();
288
289        // Set presence to online. It is important that this is done after
290        // callConnectionAuthenticatedListener(), as this call will also
291        // eventually load the roster. And we should load the roster before we
292        // send the initial presence.
293        if (config.isSendPresence()) {
294            sendPacket(new Presence(Presence.Type.available));
295        }
296    }
297
298    @Override
299    public synchronized void loginAnonymously() throws XMPPException, SmackException, SaslException, IOException {
300        if (!isConnected()) {
301            throw new NotConnectedException();
302        }
303        if (authenticated) {
304            throw new AlreadyLoggedInException();
305        }
306
307        if (saslAuthentication.hasAnonymousAuthentication()) {
308            saslAuthentication.authenticateAnonymously();
309        }
310        else {
311            throw new SaslException("No anonymous SASL authentication mechanism available");
312        }
313
314        String response = bindResourceAndEstablishSession(null);
315        // Set the user value.
316        this.user = response;
317        // Update the serviceName with the one returned by the server
318        setServiceName(StringUtils.parseServer(response));
319
320        // If compression is enabled then request the server to use stream compression
321        if (config.isCompressionEnabled()) {
322            useCompression();
323        }
324
325        // Set presence to online.
326        sendPacket(new Presence(Presence.Type.available));
327
328        // Indicate that we're now authenticated.
329        authenticated = true;
330        anonymous = true;
331
332        // If debugging is enabled, change the the debug window title to include the
333        // name we are now logged-in as.
334        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
335        // will be null
336        if (config.isDebuggerEnabled() && debugger != null) {
337            debugger.userHasLogged(user);
338        }
339        callConnectionAuthenticatedListener();
340    }
341
342    public boolean isConnected() {
343        return connected;
344    }
345
346    public boolean isSecureConnection() {
347        return isUsingTLS();
348    }
349
350    public boolean isSocketClosed() {
351        return socketClosed;
352    }
353
354    public boolean isAuthenticated() {
355        return authenticated;
356    }
357
358    public boolean isAnonymous() {
359        return anonymous;
360    }
361
362    /**
363     * Shuts the current connection down. After this method returns, the connection must be ready
364     * for re-use by connect.
365     */
366    @Override
367    protected void shutdown() {
368        if (packetReader != null) {
369                packetReader.shutdown();
370        }
371        if (packetWriter != null) {
372                packetWriter.shutdown();
373        }
374
375        // Set socketClosed to true. This will cause the PacketReader
376        // and PacketWriter to ignore any Exceptions that are thrown
377        // because of a read/write from/to a closed stream.
378        // It is *important* that this is done before socket.close()!
379        socketClosed = true;
380        try {
381                socket.close();
382        } catch (Exception e) {
383                LOGGER.log(Level.WARNING, "shutdown", e);
384        }
385
386        setWasAuthenticated(authenticated);
387        authenticated = false;
388        connected = false;
389        usingTLS = false;
390        reader = null;
391        writer = null;
392    }
393
394    @Override
395    protected void sendPacketInternal(Packet packet) throws NotConnectedException {
396        packetWriter.sendPacket(packet);
397    }
398
399    private void connectUsingConfiguration(ConnectionConfiguration config) throws SmackException, IOException {
400        Exception exception = null;
401        try {
402            maybeResolveDns();
403        }
404        catch (Exception e) {
405            throw new SmackException(e);
406        }
407        Iterator<HostAddress> it = config.getHostAddresses().iterator();
408        List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
409        while (it.hasNext()) {
410            exception = null;
411            HostAddress hostAddress = it.next();
412            String host = hostAddress.getFQDN();
413            int port = hostAddress.getPort();
414            try {
415                if (config.getSocketFactory() == null) {
416                    this.socket = new Socket(host, port);
417                }
418                else {
419                    this.socket = config.getSocketFactory().createSocket(host, port);
420                }
421            } catch (Exception e) {
422                exception = e;
423            }
424            if (exception == null) {
425                // We found a host to connect to, break here
426                host = hostAddress.getFQDN();
427                port = hostAddress.getPort();
428                break;
429            }
430            hostAddress.setException(exception);
431            failedAddresses.add(hostAddress);
432            if (!it.hasNext()) {
433                // There are no more host addresses to try
434                // throw an exception and report all tried
435                // HostAddresses in the exception
436                throw new ConnectionException(failedAddresses);
437            }
438        }
439        socketClosed = false;
440        initConnection();
441    }
442
443    /**
444     * Initializes the connection by creating a packet reader and writer and opening a
445     * XMPP stream to the server.
446     *
447     * @throws XMPPException if establishing a connection to the server fails.
448     * @throws SmackException if the server failes to respond back or if there is anther error.
449     * @throws IOException 
450     */
451    private void initConnection() throws SmackException, IOException {
452        boolean isFirstInitialization = packetReader == null || packetWriter == null;
453        compressionHandler = null;
454        serverAckdCompression = false;
455
456        // Set the reader and writer instance variables
457        initReaderAndWriter();
458
459        try {
460            if (isFirstInitialization) {
461                packetWriter = new PacketWriter(this);
462                packetReader = new PacketReader(this);
463
464                // If debugging is enabled, we should start the thread that will listen for
465                // all packets and then log them.
466                if (config.isDebuggerEnabled()) {
467                    addPacketListener(debugger.getReaderListener(), null);
468                    if (debugger.getWriterListener() != null) {
469                        addPacketSendingListener(debugger.getWriterListener(), null);
470                    }
471                }
472            }
473            else {
474                packetWriter.init();
475                packetReader.init();
476            }
477
478            // Start the packet writer. This will open a XMPP stream to the server
479            packetWriter.startup();
480            // Start the packet reader. The startup() method will block until we
481            // get an opening stream packet back from server.
482            packetReader.startup();
483
484            // Make note of the fact that we're now connected.
485            connected = true;
486
487            if (isFirstInitialization) {
488                // Notify listeners that a new connection has been established
489                for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
490                    listener.connectionCreated(this);
491                }
492            }
493
494        }
495        catch (SmackException ex) {
496            // An exception occurred in setting up the connection.
497            shutdown();
498            // Everything stoppped. Now throw the exception.
499            throw ex;
500        }
501    }
502
503    private void initReaderAndWriter() throws IOException {
504        try {
505            if (compressionHandler == null) {
506                reader =
507                        new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
508                writer = new BufferedWriter(
509                        new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
510            }
511            else {
512                try {
513                    OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
514                    writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
515
516                    InputStream is = compressionHandler.getInputStream(socket.getInputStream());
517                    reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
518                }
519                catch (Exception e) {
520                    LOGGER.log(Level.WARNING, "initReaderAndWriter()", e);
521                    compressionHandler = null;
522                    reader = new BufferedReader(
523                            new InputStreamReader(socket.getInputStream(), "UTF-8"));
524                    writer = new BufferedWriter(
525                            new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
526                }
527            }
528        }
529        catch (UnsupportedEncodingException ioe) {
530            throw new IllegalStateException(ioe);
531        }
532
533        // If debugging is enabled, we open a window and write out all network traffic.
534        initDebugger();
535    }
536
537    /***********************************************
538     * TLS code below
539     **********************************************/
540
541    /**
542     * Returns true if the connection to the server has successfully negotiated TLS. Once TLS
543     * has been negotiatied the connection has been secured.
544     *
545     * @return true if the connection to the server has successfully negotiated TLS.
546     */
547    public boolean isUsingTLS() {
548        return usingTLS;
549    }
550
551    /**
552     * Notification message saying that the server supports TLS so confirm the server that we
553     * want to secure the connection.
554     *
555     * @param required true when the server indicates that TLS is required.
556     * @throws IOException if an exception occurs.
557     */
558    void startTLSReceived(boolean required) throws IOException {
559        if (required && config.getSecurityMode() ==
560                ConnectionConfiguration.SecurityMode.disabled) {
561            notifyConnectionError(new IllegalStateException(
562                    "TLS required by server but not allowed by connection configuration"));
563            return;
564        }
565
566        if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
567            // Do not secure the connection using TLS since TLS was disabled
568            return;
569        }
570        writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
571        writer.flush();
572    }
573
574    /**
575     * The server has indicated that TLS negotiation can start. We now need to secure the
576     * existing plain connection and perform a handshake. This method won't return until the
577     * connection has finished the handshake or an error occurred while securing the connection.
578     *
579     * @throws Exception if an exception occurs.
580     */
581    void proceedTLSReceived() throws Exception {
582        SSLContext context = this.config.getCustomSSLContext();
583        KeyStore ks = null;
584        KeyManager[] kms = null;
585        PasswordCallback pcb = null;
586
587        if(config.getCallbackHandler() == null) {
588           ks = null;
589        } else if (context == null) {
590            if(config.getKeystoreType().equals("NONE")) {
591                ks = null;
592                pcb = null;
593            }
594            else if(config.getKeystoreType().equals("PKCS11")) {
595                try {
596                    Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
597                    String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library();
598                    ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes());
599                    Provider p = (Provider)c.newInstance(config);
600                    Security.addProvider(p);
601                    ks = KeyStore.getInstance("PKCS11",p);
602                    pcb = new PasswordCallback("PKCS11 Password: ",false);
603                    this.config.getCallbackHandler().handle(new Callback[]{pcb});
604                    ks.load(null,pcb.getPassword());
605                }
606                catch (Exception e) {
607                    ks = null;
608                    pcb = null;
609                }
610            }
611            else if(config.getKeystoreType().equals("Apple")) {
612                ks = KeyStore.getInstance("KeychainStore","Apple");
613                ks.load(null,null);
614                //pcb = new PasswordCallback("Apple Keychain",false);
615                //pcb.setPassword(null);
616            }
617            else {
618                ks = KeyStore.getInstance(config.getKeystoreType());
619                try {
620                    pcb = new PasswordCallback("Keystore Password: ",false);
621                    config.getCallbackHandler().handle(new Callback[]{pcb});
622                    ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword());
623                }
624                catch(Exception e) {
625                    ks = null;
626                    pcb = null;
627                }
628            }
629            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
630            try {
631                if(pcb == null) {
632                    kmf.init(ks,null);
633                } else {
634                    kmf.init(ks,pcb.getPassword());
635                    pcb.clearPassword();
636                }
637                kms = kmf.getKeyManagers();
638            } catch (NullPointerException npe) {
639                kms = null;
640            }
641        }
642
643        // If the user didn't specify a SSLContext, use the default one
644        if (context == null) {
645            context = SSLContext.getInstance("TLS");
646            context.init(kms, null, new java.security.SecureRandom());
647        }
648        Socket plain = socket;
649        // Secure the plain connection
650        socket = context.getSocketFactory().createSocket(plain,
651                plain.getInetAddress().getHostAddress(), plain.getPort(), true);
652        // Initialize the reader and writer with the new secured version
653        initReaderAndWriter();
654
655        final SSLSocket sslSocket = (SSLSocket) socket;
656        try {
657            // Proceed to do the handshake
658            sslSocket.startHandshake();
659        }
660        catch (IOException e) {
661            setConnectionException(e);
662            throw e;
663        }
664
665        final HostnameVerifier verifier = getConfiguration().getHostnameVerifier();
666        if (verifier != null && !verifier.verify(getServiceName(), sslSocket.getSession())) {
667            throw new CertificateException("Hostname verification of certificate failed. Certificate does not authenticate " + getServiceName());
668        }
669
670        //if (((SSLSocket) socket).getWantClientAuth()) {
671        //    System.err.println("XMPPConnection wants client auth");
672        //}
673        //else if (((SSLSocket) socket).getNeedClientAuth()) {
674        //    System.err.println("XMPPConnection needs client auth");
675        //}
676        //else {
677        //    System.err.println("XMPPConnection does not require client auth");
678       // }
679        // Set that TLS was successful
680        usingTLS = true;
681
682        // Set the new  writer to use
683        packetWriter.setWriter(writer);
684        // Send a new opening stream to the server
685        packetWriter.openStream();
686    }
687
688    /**
689     * Sets the available stream compression methods offered by the server.
690     *
691     * @param methods compression methods offered by the server.
692     */
693    void setAvailableCompressionMethods(Collection<String> methods) {
694        compressionMethods = methods;
695    }
696
697    /**
698     * Returns the compression handler that can be used for one compression methods offered by the server.
699     * 
700     * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
701     * 
702     */
703    private XMPPInputOutputStream maybeGetCompressionHandler() {
704        if (compressionMethods != null) {
705            for (XMPPInputOutputStream handler : SmackConfiguration.getCompresionHandlers()) {
706                String method = handler.getCompressionMethod();
707                if (compressionMethods.contains(method))
708                    return handler;
709            }
710        }
711        return null;
712    }
713
714    public boolean isUsingCompression() {
715        return compressionHandler != null && serverAckdCompression;
716    }
717
718    /**
719     * Starts using stream compression that will compress network traffic. Traffic can be
720     * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
721     * connection. However, the server and the client will need to use more CPU time in order to
722     * un/compress network data so under high load the server performance might be affected.
723     * <p>
724     * <p>
725     * Stream compression has to have been previously offered by the server. Currently only the
726     * zlib method is supported by the client. Stream compression negotiation has to be done
727     * before authentication took place.<p>
728     * <p>
729     *
730     * @return true if stream compression negotiation was successful.
731     * @throws IOException if the compress stanza could not be send
732     */
733    private boolean useCompression() throws IOException {
734        // If stream compression was offered by the server and we want to use
735        // compression then send compression request to the server
736        if (authenticated) {
737            throw new IllegalStateException("Compression should be negotiated before authentication.");
738        }
739
740        if ((compressionHandler = maybeGetCompressionHandler()) != null) {
741            synchronized (compressionLock) {
742                requestStreamCompression(compressionHandler.getCompressionMethod());
743                // Wait until compression is being used or a timeout happened
744                try {
745                    compressionLock.wait(getPacketReplyTimeout());
746                }
747                catch (InterruptedException e) {
748                    // Ignore.
749                }
750            }
751            return isUsingCompression();
752        }
753        return false;
754    }
755
756    /**
757     * Request the server that we want to start using stream compression. When using TLS
758     * then negotiation of stream compression can only happen after TLS was negotiated. If TLS
759     * compression is being used the stream compression should not be used.
760     * @throws IOException if the compress stanza could not be send
761     */
762    private void requestStreamCompression(String method) throws IOException {
763        writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
764        writer.write("<method>" + method + "</method></compress>");
765        writer.flush();
766    }
767
768    /**
769     * Start using stream compression since the server has acknowledged stream compression.
770     *
771     * @throws IOException if there is an exception starting stream compression.
772     */
773    void startStreamCompression() throws IOException {
774        serverAckdCompression = true;
775        // Initialize the reader and writer with the new secured version
776        initReaderAndWriter();
777
778        // Set the new  writer to use
779        packetWriter.setWriter(writer);
780        // Send a new opening stream to the server
781        packetWriter.openStream();
782        // Notify that compression is being used
783        streamCompressionNegotiationDone();
784    }
785
786    /**
787     * Notifies the XMPP connection that stream compression negotiation is done so that the
788     * connection process can proceed.
789     */
790    void streamCompressionNegotiationDone() {
791        synchronized (compressionLock) {
792            compressionLock.notify();
793        }
794    }
795
796    /**
797     * Establishes a connection to the XMPP server and performs an automatic login
798     * only if the previous connection state was logged (authenticated). It basically
799     * creates and maintains a socket connection to the server.<p>
800     * <p/>
801     * Listeners will be preserved from a previous connection if the reconnection
802     * occurs after an abrupt termination.
803     *
804     * @throws XMPPException if an error occurs while trying to establish the connection.
805     * @throws SmackException 
806     * @throws IOException 
807     */
808    @Override
809    protected void connectInternal() throws SmackException, IOException, XMPPException {
810        // Establishes the connection, readers and writers
811        connectUsingConfiguration(config);
812        // TODO is there a case where connectUsing.. does not throw an exception but connected is
813        // still false?
814        if (connected) {
815            callConnectionConnectedListener();
816        }
817        // Automatically makes the login if the user was previously connected successfully
818        // to the server and the connection was terminated abruptly
819        if (connected && wasAuthenticated) {
820            // Make the login
821            if (isAnonymous()) {
822                // Make the anonymous login
823                loginAnonymously();
824            }
825            else {
826                login(config.getUsername(), config.getPassword(), config.getResource());
827            }
828            notifyReconnection();
829        }
830    }
831
832    /**
833     * Sends out a notification that there was an error with the connection
834     * and closes the connection. Also prints the stack trace of the given exception
835     *
836     * @param e the exception that causes the connection close event.
837     */
838    synchronized void notifyConnectionError(Exception e) {
839        // Listeners were already notified of the exception, return right here.
840        if ((packetReader == null || packetReader.done) &&
841                (packetWriter == null || packetWriter.done)) return;
842
843        // Closes the connection temporary. A reconnection is possible
844        shutdown();
845
846        // Notify connection listeners of the error.
847        callConnectionClosedOnErrorListener(e);
848    }
849
850    @Override
851    protected void processPacket(Packet packet) {
852        super.processPacket(packet);
853    }
854
855    @Override
856    protected Reader getReader() {
857        return super.getReader();
858    }
859
860    @Override
861    protected Writer getWriter() {
862        return super.getWriter();
863    }
864
865    @Override
866    protected void throwConnectionExceptionOrNoResponse() throws IOException, NoResponseException {
867        super.throwConnectionExceptionOrNoResponse();
868    }
869
870    @Override
871    protected void setServiceName(String serviceName) {
872        super.setServiceName(serviceName);
873    }
874
875    @Override
876    protected void serverRequiresBinding() {
877        super.serverRequiresBinding();
878    }
879
880    @Override
881    protected void setServiceCapsNode(String node) {
882        super.setServiceCapsNode(node);
883    }
884
885    @Override
886    protected void serverSupportsSession() {
887        super.serverSupportsSession();
888    }
889
890    @Override
891    protected void setRosterVersioningSupported() {
892        super.setRosterVersioningSupported();
893    }
894
895    @Override
896    protected void serverSupportsAccountCreation() {
897        super.serverSupportsAccountCreation();
898    }
899
900    @Override
901    protected SASLAuthentication getSASLAuthentication() {
902        return super.getSASLAuthentication();
903    }
904
905    @Override
906    protected ConnectionConfiguration getConfiguration() {
907        return super.getConfiguration();
908    }
909
910    /**
911     * Sends a notification indicating that the connection was reconnected successfully.
912     */
913    private void notifyReconnection() {
914        // Notify connection listeners of the reconnection.
915        for (ConnectionListener listener : getConnectionListeners()) {
916            try {
917                listener.reconnectionSuccessful();
918            }
919            catch (Exception e) {
920                // Catch and print any exception so we can recover
921                // from a faulty listener
922                LOGGER.log(Level.WARNING, "notifyReconnection()", e);
923            }
924        }
925    }
926}