001/**
002 *
003 * Copyright 2009 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 */
017
018package org.jivesoftware.smack.bosh;
019
020import java.io.IOException;
021import java.io.PipedReader;
022import java.io.PipedWriter;
023import java.io.Writer;
024import java.util.Locale;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import javax.security.sasl.SaslException;
029
030import org.jivesoftware.smack.SmackException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.SmackException.AlreadyLoggedInException;
033import org.jivesoftware.smack.SmackException.ConnectionException;
034import org.jivesoftware.smack.SASLAuthentication;
035import org.jivesoftware.smack.XMPPConnection;
036import org.jivesoftware.smack.ConnectionCreationListener;
037import org.jivesoftware.smack.ConnectionListener;
038import org.jivesoftware.smack.Roster;
039import org.jivesoftware.smack.XMPPException;
040import org.jivesoftware.smack.packet.Packet;
041import org.jivesoftware.smack.packet.Presence;
042import org.jivesoftware.smack.packet.Presence.Type;
043import org.jivesoftware.smack.util.StringUtils;
044import org.igniterealtime.jbosh.BOSHClient;
045import org.igniterealtime.jbosh.BOSHClientConfig;
046import org.igniterealtime.jbosh.BOSHClientConnEvent;
047import org.igniterealtime.jbosh.BOSHClientConnListener;
048import org.igniterealtime.jbosh.BOSHClientRequestListener;
049import org.igniterealtime.jbosh.BOSHClientResponseListener;
050import org.igniterealtime.jbosh.BOSHException;
051import org.igniterealtime.jbosh.BOSHMessageEvent;
052import org.igniterealtime.jbosh.BodyQName;
053import org.igniterealtime.jbosh.ComposableBody;
054
055/**
056 * Creates a connection to a XMPP server via HTTP binding.
057 * This is specified in the XEP-0206: XMPP Over BOSH.
058 * 
059 * @see XMPPConnection
060 * @author Guenther Niess
061 */
062public class XMPPBOSHConnection extends XMPPConnection {
063    private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName());
064
065    /**
066     * The XMPP Over Bosh namespace.
067     */
068    public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
069
070    /**
071     * The BOSH namespace from XEP-0124.
072     */
073    public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
074
075    /**
076     * The used BOSH client from the jbosh library.
077     */
078    private BOSHClient client;
079
080    /**
081     * Holds the initial configuration used while creating the connection.
082     */
083    private final BOSHConfiguration config;
084
085    // Some flags which provides some info about the current state.
086    private boolean connected = false;
087    private boolean authenticated = false;
088    private boolean anonymous = false;
089    private boolean isFirstInitialization = true;
090    private boolean wasAuthenticated = false;
091    private boolean done = false;
092
093    // The readerPipe and consumer thread are used for the debugger.
094    private PipedWriter readerPipe;
095    private Thread readerConsumer;
096
097    /**
098     * The BOSH equivalent of the stream ID which is used for DIGEST authentication.
099     */
100    protected String authID = null;
101
102    /**
103     * The session ID for the BOSH session with the connection manager.
104     */
105    protected String sessionID = null;
106
107    /**
108     * The full JID of the authenticated user.
109     */
110    private String user = null;
111
112    /**
113     * Create a HTTP Binding connection to a XMPP server.
114     * 
115     * @param https true if you want to use SSL
116     *             (e.g. false for http://domain.lt:7070/http-bind).
117     * @param host the hostname or IP address of the connection manager
118     *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
119     * @param port the port of the connection manager
120     *             (e.g. 7070 for http://domain.lt:7070/http-bind).
121     * @param filePath the file which is described by the URL
122     *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
123     * @param xmppDomain the XMPP service name
124     *             (e.g. domain.lt for the user alice@domain.lt)
125     */
126    public XMPPBOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
127        super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
128        this.config = (BOSHConfiguration) getConfiguration();
129    }
130
131    /**
132     * Create a HTTP Binding connection to a XMPP server.
133     * 
134     * @param config The configuration which is used for this connection.
135     */
136    public XMPPBOSHConnection(BOSHConfiguration config) {
137        super(config);
138        this.config = config;
139    }
140
141    @Override
142    protected void connectInternal() throws SmackException {
143        if (connected) {
144            throw new IllegalStateException("Already connected to a server.");
145        }
146        done = false;
147        try {
148            // Ensure a clean starting state
149            if (client != null) {
150                client.close();
151                client = null;
152            }
153            sessionID = null;
154            authID = null;
155
156            // Initialize BOSH client
157            BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
158                    .create(config.getURI(), config.getServiceName());
159            if (config.isProxyEnabled()) {
160                cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
161            }
162            client = BOSHClient.create(cfgBuilder.build());
163
164            client.addBOSHClientConnListener(new BOSHConnectionListener(this));
165            client.addBOSHClientResponseListener(new BOSHPacketReader(this));
166
167            // Initialize the debugger
168            if (config.isDebuggerEnabled()) {
169                initDebugger();
170                if (isFirstInitialization) {
171                    if (debugger.getReaderListener() != null) {
172                        addPacketListener(debugger.getReaderListener(), null);
173                    }
174                    if (debugger.getWriterListener() != null) {
175                        addPacketSendingListener(debugger.getWriterListener(), null);
176                    }
177                }
178            }
179
180            // Send the session creation request
181            client.send(ComposableBody.builder()
182                    .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
183                    .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
184                    .build());
185        } catch (Exception e) {
186            throw new ConnectionException(e);
187        }
188
189        // Wait for the response from the server
190        synchronized (this) {
191            if (!connected) {
192                try {
193                    wait(getPacketReplyTimeout());
194                }
195                catch (InterruptedException e) {}
196            }
197        }
198
199        // If there is no feedback, throw an remote server timeout error
200        if (!connected && !done) {
201            done = true;
202            String errorMessage = "Timeout reached for the connection to " 
203                    + getHost() + ":" + getPort() + ".";
204            throw new SmackException(errorMessage);
205        }
206        callConnectionConnectedListener();
207    }
208
209    public String getConnectionID() {
210        if (!connected) {
211            return null;
212        } else if (authID != null) {
213            return authID;
214        } else {
215            return sessionID;
216        }
217    }
218
219    public String getUser() {
220        return user;
221    }
222
223    public boolean isAnonymous() {
224        return anonymous;
225    }
226
227    public boolean isAuthenticated() {
228        return authenticated;
229    }
230
231    public boolean isConnected() {
232        return connected;
233    }
234
235    public boolean isSecureConnection() {
236        // TODO: Implement SSL usage
237        return false;
238    }
239
240    public boolean isUsingCompression() {
241        // TODO: Implement compression
242        return false;
243    }
244
245    public void login(String username, String password, String resource)
246            throws XMPPException, SmackException, IOException {
247        if (!isConnected()) {
248            throw new NotConnectedException();
249        }
250        if (authenticated) {
251            throw new AlreadyLoggedInException();
252        }
253        // Do partial version of nameprep on the username.
254        username = username.toLowerCase(Locale.US).trim();
255
256        if (saslAuthentication.hasNonAnonymousAuthentication()) {
257            // Authenticate using SASL
258            if (password != null) {
259                 saslAuthentication.authenticate(username, password, resource);
260            } else {
261                saslAuthentication.authenticate(resource, config.getCallbackHandler());
262            }
263        } else {
264            throw new SaslException("No non-anonymous SASL authentication mechanism available");
265        }
266
267        String response = bindResourceAndEstablishSession(resource);
268        // Set the user.
269        if (response != null) {
270            this.user = response;
271            // Update the serviceName with the one returned by the server
272            setServiceName(StringUtils.parseServer(response));
273        } else {
274            this.user = username + "@" + getServiceName();
275            if (resource != null) {
276                this.user += "/" + resource;
277            }
278        }
279
280        // Indicate that we're now authenticated.
281        authenticated = true;
282        anonymous = false;
283
284        // Stores the autentication for future reconnection
285        setLoginInfo(username, password, resource);
286
287        // If debugging is enabled, change the the debug window title to include
288        // the
289        // name we are now logged-in as.l
290        if (config.isDebuggerEnabled() && debugger != null) {
291            debugger.userHasLogged(user);
292        }
293        callConnectionAuthenticatedListener();
294
295        // Set presence to online. It is important that this is done after
296        // callConnectionAuthenticatedListener(), as this call will also
297        // eventually load the roster. And we should load the roster before we
298        // send the initial presence.
299        if (config.isSendPresence()) {
300            sendPacket(new Presence(Presence.Type.available));
301        }
302    }
303
304    public void loginAnonymously() throws XMPPException, SmackException, IOException {
305        if (!isConnected()) {
306            throw new NotConnectedException();
307        }
308        if (authenticated) {
309            throw new AlreadyLoggedInException();
310        }
311
312        if (saslAuthentication.hasAnonymousAuthentication()) {
313            saslAuthentication.authenticateAnonymously();
314        }
315        else {
316            // Authenticate using Non-SASL
317            throw new SaslException("No anonymous SASL authentication mechanism available");
318        }
319
320        String response = bindResourceAndEstablishSession(null);
321        // Set the user value.
322        this.user = response;
323        // Update the serviceName with the one returned by the server
324        setServiceName(StringUtils.parseServer(response));
325
326        // Set presence to online.
327        if (config.isSendPresence()) {
328            sendPacket(new Presence(Presence.Type.available));
329        }
330
331        // Indicate that we're now authenticated.
332        authenticated = true;
333        anonymous = true;
334
335        // If debugging is enabled, change the the debug window title to include the
336        // name we are now logged-in as.
337        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
338        // will be null
339        if (config.isDebuggerEnabled() && debugger != null) {
340            debugger.userHasLogged(user);
341        }
342        callConnectionAuthenticatedListener();
343    }
344
345    @Override
346    protected void sendPacketInternal(Packet packet) throws NotConnectedException {
347        if (done) {
348            throw new NotConnectedException();
349        }
350        try {
351            send(ComposableBody.builder().setPayloadXML(packet.toXML().toString()).build());
352        }
353        catch (BOSHException e) {
354            LOGGER.log(Level.SEVERE, "BOSHException in sendPacketInternal", e);
355        }
356    }
357
358    /**
359     * Closes the connection by setting presence to unavailable and closing the 
360     * HTTP client. The shutdown logic will be used during a planned disconnection or when
361     * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
362     * BOSH packet reader and {@link Roster} will not be removed; thus
363     * connection's state is kept.
364     *
365     */
366    @Override
367    protected void shutdown() {
368        setWasAuthenticated(authenticated);
369        authID = null;
370        sessionID = null;
371        done = true;
372        authenticated = false;
373        connected = false;
374        isFirstInitialization = false;
375
376        Presence unavailablePresence = new Presence(Type.unavailable);
377        try {
378            client.disconnect(ComposableBody.builder()
379                    .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
380                    .setPayloadXML(unavailablePresence.toXML().toString())
381                    .build());
382            // Wait 150 ms for processes to clean-up, then shutdown.
383            Thread.sleep(150);
384        }
385        catch (Exception e) {
386            // Ignore.
387        }
388
389        // Close down the readers and writers.
390        if (readerPipe != null) {
391            try {
392                readerPipe.close();
393            }
394            catch (Throwable ignore) { /* ignore */ }
395            reader = null;
396        }
397        if (reader != null) {
398            try {
399                reader.close();
400            }
401            catch (Throwable ignore) { /* ignore */ }
402            reader = null;
403        }
404        if (writer != null) {
405            try {
406                writer.close();
407            }
408            catch (Throwable ignore) { /* ignore */ }
409            writer = null;
410        }
411
412        readerConsumer = null;
413    }
414
415    /**
416     * Send a HTTP request to the connection manager with the provided body element.
417     * 
418     * @param body the body which will be sent.
419     */
420    protected void send(ComposableBody body) throws BOSHException {
421        if (!connected) {
422            throw new IllegalStateException("Not connected to a server!");
423        }
424        if (body == null) {
425            throw new NullPointerException("Body mustn't be null!");
426        }
427        if (sessionID != null) {
428            body = body.rebuild().setAttribute(
429                    BodyQName.create(BOSH_URI, "sid"), sessionID).build();
430        }
431        client.send(body);
432    }
433
434    /**
435     * Initialize the SmackDebugger which allows to log and debug XML traffic.
436     */
437    protected void initDebugger() {
438        // TODO: Maybe we want to extend the SmackDebugger for simplification
439        //       and a performance boost.
440
441        // Initialize a empty writer which discards all data.
442        writer = new Writer() {
443                public void write(char[] cbuf, int off, int len) { /* ignore */}
444                public void close() { /* ignore */ }
445                public void flush() { /* ignore */ }
446            };
447
448        // Initialize a pipe for received raw data.
449        try {
450            readerPipe = new PipedWriter();
451            reader = new PipedReader(readerPipe);
452        }
453        catch (IOException e) {
454            // Ignore
455        }
456
457        // Call the method from the parent class which initializes the debugger.
458        super.initDebugger();
459
460        // Add listeners for the received and sent raw data.
461        client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
462            public void responseReceived(BOSHMessageEvent event) {
463                if (event.getBody() != null) {
464                    try {
465                        readerPipe.write(event.getBody().toXML());
466                        readerPipe.flush();
467                    } catch (Exception e) {
468                        // Ignore
469                    }
470                }
471            }
472        });
473        client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
474            public void requestSent(BOSHMessageEvent event) {
475                if (event.getBody() != null) {
476                    try {
477                        writer.write(event.getBody().toXML());
478                    } catch (Exception e) {
479                        // Ignore
480                    }
481                }
482            }
483        });
484
485        // Create and start a thread which discards all read data.
486        readerConsumer = new Thread() {
487            private Thread thread = this;
488            private int bufferLength = 1024;
489
490            public void run() {
491                try {
492                    char[] cbuf = new char[bufferLength];
493                    while (readerConsumer == thread && !done) {
494                        reader.read(cbuf, 0, bufferLength);
495                    }
496                } catch (IOException e) {
497                    // Ignore
498                }
499            }
500        };
501        readerConsumer.setDaemon(true);
502        readerConsumer.start();
503    }
504
505    /**
506     * Sends out a notification that there was an error with the connection
507     * and closes the connection.
508     *
509     * @param e the exception that causes the connection close event.
510     */
511    protected void notifyConnectionError(Exception e) {
512        // Closes the connection temporary. A reconnection is possible
513        shutdown();
514        callConnectionClosedOnErrorListener(e);
515    }
516
517    @Override
518    protected void processPacket(Packet packet) {
519        super.processPacket(packet);
520    }
521
522    @Override
523    protected SASLAuthentication getSASLAuthentication() {
524        return super.getSASLAuthentication();
525    }
526
527    @Override
528    protected void serverRequiresBinding() {
529        super.serverRequiresBinding();
530    }
531
532    @Override
533    protected void serverSupportsSession() {
534        super.serverSupportsSession();
535    }
536
537    @Override
538    protected void serverSupportsAccountCreation() {
539        super.serverSupportsAccountCreation();
540    }
541
542    /**
543     * A listener class which listen for a successfully established connection
544     * and connection errors and notifies the BOSHConnection.
545     * 
546     * @author Guenther Niess
547     */
548    private class BOSHConnectionListener implements BOSHClientConnListener {
549
550        private final XMPPBOSHConnection connection;
551
552        public BOSHConnectionListener(XMPPBOSHConnection connection) {
553            this.connection = connection;
554        }
555
556        /**
557         * Notify the BOSHConnection about connection state changes.
558         * Process the connection listeners and try to login if the
559         * connection was formerly authenticated and is now reconnected.
560         */
561        public void connectionEvent(BOSHClientConnEvent connEvent) {
562            try {
563                if (connEvent.isConnected()) {
564                    connected = true;
565                    if (isFirstInitialization) {
566                        isFirstInitialization = false;
567                        for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
568                            listener.connectionCreated(connection);
569                        }
570                    }
571                    else {
572                        try {
573                            if (wasAuthenticated) {
574                                connection.login(
575                                        config.getUsername(),
576                                        config.getPassword(),
577                                        config.getResource());
578                            }
579                            for (ConnectionListener listener : getConnectionListeners()) {
580                                 listener.reconnectionSuccessful();
581                            }
582                        }
583                        catch (Exception e) {
584                            for (ConnectionListener listener : getConnectionListeners()) {
585                                listener.reconnectionFailed(e);
586                           }
587                        }
588                    }
589                }
590                else {
591                    if (connEvent.isError()) {
592                        // TODO Check why jbosh's getCause returns Throwable here. This is very
593                        // unusual and should be avoided if possible
594                        Throwable cause = connEvent.getCause();
595                        Exception e;
596                        if (cause instanceof Exception) {
597                            e = (Exception) cause;
598                        } else {
599                            e = new Exception(cause);
600                        }
601                        notifyConnectionError(e);
602                    }
603                    connected = false;
604                }
605            }
606            finally {
607                synchronized (connection) {
608                    connection.notifyAll();
609                }
610            }
611        }
612    }
613}