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.StringReader;
024import java.io.Writer;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.jivesoftware.smack.AbstractXMPPConnection;
029import org.jivesoftware.smack.SmackException;
030import org.jivesoftware.smack.SmackException.NotConnectedException;
031import org.jivesoftware.smack.SmackException.ConnectionException;
032import org.jivesoftware.smack.XMPPException.StreamErrorException;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.ConnectionCreationListener;
035import org.jivesoftware.smack.XMPPException;
036import org.jivesoftware.smack.packet.Element;
037import org.jivesoftware.smack.packet.IQ;
038import org.jivesoftware.smack.packet.Message;
039import org.jivesoftware.smack.packet.Stanza;
040import org.jivesoftware.smack.packet.PlainStreamElement;
041import org.jivesoftware.smack.packet.Presence;
042import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
043import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
044import org.jivesoftware.smack.util.PacketParserUtils;
045import org.xmlpull.v1.XmlPullParser;
046import org.xmlpull.v1.XmlPullParserFactory;
047import org.igniterealtime.jbosh.AbstractBody;
048import org.igniterealtime.jbosh.BOSHClient;
049import org.igniterealtime.jbosh.BOSHClientConfig;
050import org.igniterealtime.jbosh.BOSHClientConnEvent;
051import org.igniterealtime.jbosh.BOSHClientConnListener;
052import org.igniterealtime.jbosh.BOSHClientRequestListener;
053import org.igniterealtime.jbosh.BOSHClientResponseListener;
054import org.igniterealtime.jbosh.BOSHException;
055import org.igniterealtime.jbosh.BOSHMessageEvent;
056import org.igniterealtime.jbosh.BodyQName;
057import org.igniterealtime.jbosh.ComposableBody;
058
059/**
060 * Creates a connection to an XMPP server via HTTP binding.
061 * This is specified in the XEP-0206: XMPP Over BOSH.
062 * 
063 * @see XMPPConnection
064 * @author Guenther Niess
065 */
066public class XMPPBOSHConnection extends AbstractXMPPConnection {
067    private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName());
068
069    /**
070     * The XMPP Over Bosh namespace.
071     */
072    public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
073
074    /**
075     * The BOSH namespace from XEP-0124.
076     */
077    public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
078
079    /**
080     * The used BOSH client from the jbosh library.
081     */
082    private BOSHClient client;
083
084    /**
085     * Holds the initial configuration used while creating the connection.
086     */
087    private final BOSHConfiguration config;
088
089    // Some flags which provides some info about the current state.
090    private boolean isFirstInitialization = true;
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 session ID for the BOSH session with the connection manager.
099     */
100    protected String sessionID = null;
101
102    /**
103     * Create a HTTP Binding connection to an XMPP server.
104     * 
105     * @param username the username to use.
106     * @param password the password to use.
107     * @param https true if you want to use SSL
108     *             (e.g. false for http://domain.lt:7070/http-bind).
109     * @param host the hostname or IP address of the connection manager
110     *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
111     * @param port the port of the connection manager
112     *             (e.g. 7070 for http://domain.lt:7070/http-bind).
113     * @param filePath the file which is described by the URL
114     *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
115     * @param xmppDomain the XMPP service name
116     *             (e.g. domain.lt for the user alice@domain.lt)
117     */
118    public XMPPBOSHConnection(String username, String password, boolean https, String host, int port, String filePath, String xmppDomain) {
119        this(BOSHConfiguration.builder().setUseHttps(https).setHost(host)
120                .setPort(port).setFile(filePath).setServiceName(xmppDomain)
121                .setUsernameAndPassword(username, password).build());
122    }
123
124    /**
125     * Create a HTTP Binding connection to an XMPP server.
126     * 
127     * @param config The configuration which is used for this connection.
128     */
129    public XMPPBOSHConnection(BOSHConfiguration config) {
130        super(config);
131        this.config = config;
132    }
133
134    @Override
135    protected void connectInternal() throws SmackException {
136        done = false;
137        try {
138            // Ensure a clean starting state
139            if (client != null) {
140                client.close();
141                client = null;
142            }
143            sessionID = null;
144
145            // Initialize BOSH client
146            BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
147                    .create(config.getURI(), config.getServiceName());
148            if (config.isProxyEnabled()) {
149                cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
150            }
151            client = BOSHClient.create(cfgBuilder.build());
152
153            client.addBOSHClientConnListener(new BOSHConnectionListener());
154            client.addBOSHClientResponseListener(new BOSHPacketReader());
155
156            // Initialize the debugger
157            if (config.isDebuggerEnabled()) {
158                initDebugger();
159                if (isFirstInitialization) {
160                    if (debugger.getReaderListener() != null) {
161                        addAsyncStanzaListener(debugger.getReaderListener(), null);
162                    }
163                    if (debugger.getWriterListener() != null) {
164                        addPacketSendingListener(debugger.getWriterListener(), null);
165                    }
166                }
167            }
168
169            // Send the session creation request
170            client.send(ComposableBody.builder()
171                    .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
172                    .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
173                    .build());
174        } catch (Exception e) {
175            throw new ConnectionException(e);
176        }
177
178        // Wait for the response from the server
179        synchronized (this) {
180            if (!connected) {
181                try {
182                    wait(getPacketReplyTimeout());
183                }
184                catch (InterruptedException e) {}
185            }
186        }
187
188        // If there is no feedback, throw an remote server timeout error
189        if (!connected && !done) {
190            done = true;
191            String errorMessage = "Timeout reached for the connection to " 
192                    + getHost() + ":" + getPort() + ".";
193            throw new SmackException(errorMessage);
194        }
195
196        // Wait with SASL auth until the SASL mechanisms have been received
197        saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
198
199        callConnectionConnectedListener();
200    }
201
202    public boolean isSecureConnection() {
203        // TODO: Implement SSL usage
204        return false;
205    }
206
207    public boolean isUsingCompression() {
208        // TODO: Implement compression
209        return false;
210    }
211
212    @Override
213    protected void loginNonAnonymously(String username, String password, String resource)
214            throws XMPPException, SmackException, IOException {
215        if (saslAuthentication.hasNonAnonymousAuthentication()) {
216            // Authenticate using SASL
217            if (password != null) {
218                 saslAuthentication.authenticate(username, password, resource);
219            } else {
220                saslAuthentication.authenticate(resource, config.getCallbackHandler());
221            }
222        } else {
223            throw new SmackException("No non-anonymous SASL authentication mechanism available");
224        }
225
226        bindResourceAndEstablishSession(resource);
227
228        afterSuccessfulLogin(false);
229    }
230
231    @Override
232    protected void loginAnonymously() throws XMPPException, SmackException, IOException {
233        // Wait with SASL auth until the SASL mechanisms have been received
234        saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
235
236        if (saslAuthentication.hasAnonymousAuthentication()) {
237            saslAuthentication.authenticateAnonymously();
238        }
239        else {
240            // Authenticate using Non-SASL
241            throw new SmackException("No anonymous SASL authentication mechanism available");
242        }
243
244        bindResourceAndEstablishSession(null);
245
246        afterSuccessfulLogin(false);
247    }
248
249    @Override
250    public void send(PlainStreamElement element) throws NotConnectedException {
251        if (done) {
252            throw new NotConnectedException();
253        }
254        sendElement(element);
255    }
256
257    @Override
258    protected void sendStanzaInternal(Stanza packet) throws NotConnectedException {
259        sendElement(packet);
260    }
261
262    private void sendElement(Element element) {
263        try {
264            send(ComposableBody.builder().setPayloadXML(element.toXML().toString()).build());
265            if (element instanceof Stanza) {
266                firePacketSendingListeners((Stanza) element);
267            }
268        }
269        catch (BOSHException e) {
270            LOGGER.log(Level.SEVERE, "BOSHException in sendStanzaInternal", e);
271        }
272    }
273
274    /**
275     * Closes the connection by setting presence to unavailable and closing the 
276     * HTTP client. The shutdown logic will be used during a planned disconnection or when
277     * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
278     * BOSH stanza(/packet) reader will not be removed; thus connection's state is kept.
279     *
280     */
281    @Override
282    protected void shutdown() {
283        setWasAuthenticated();
284        sessionID = null;
285        done = true;
286        authenticated = false;
287        connected = false;
288        isFirstInitialization = false;
289
290        // Close down the readers and writers.
291        if (readerPipe != null) {
292            try {
293                readerPipe.close();
294            }
295            catch (Throwable ignore) { /* ignore */ }
296            reader = null;
297        }
298        if (reader != null) {
299            try {
300                reader.close();
301            }
302            catch (Throwable ignore) { /* ignore */ }
303            reader = null;
304        }
305        if (writer != null) {
306            try {
307                writer.close();
308            }
309            catch (Throwable ignore) { /* ignore */ }
310            writer = null;
311        }
312
313        readerConsumer = null;
314    }
315
316    /**
317     * Send a HTTP request to the connection manager with the provided body element.
318     * 
319     * @param body the body which will be sent.
320     */
321    protected void send(ComposableBody body) throws BOSHException {
322        if (!connected) {
323            throw new IllegalStateException("Not connected to a server!");
324        }
325        if (body == null) {
326            throw new NullPointerException("Body mustn't be null!");
327        }
328        if (sessionID != null) {
329            body = body.rebuild().setAttribute(
330                    BodyQName.create(BOSH_URI, "sid"), sessionID).build();
331        }
332        client.send(body);
333    }
334
335    /**
336     * Initialize the SmackDebugger which allows to log and debug XML traffic.
337     */
338    protected void initDebugger() {
339        // TODO: Maybe we want to extend the SmackDebugger for simplification
340        //       and a performance boost.
341
342        // Initialize a empty writer which discards all data.
343        writer = new Writer() {
344                public void write(char[] cbuf, int off, int len) { /* ignore */}
345                public void close() { /* ignore */ }
346                public void flush() { /* ignore */ }
347            };
348
349        // Initialize a pipe for received raw data.
350        try {
351            readerPipe = new PipedWriter();
352            reader = new PipedReader(readerPipe);
353        }
354        catch (IOException e) {
355            // Ignore
356        }
357
358        // Call the method from the parent class which initializes the debugger.
359        super.initDebugger();
360
361        // Add listeners for the received and sent raw data.
362        client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
363            public void responseReceived(BOSHMessageEvent event) {
364                if (event.getBody() != null) {
365                    try {
366                        readerPipe.write(event.getBody().toXML());
367                        readerPipe.flush();
368                    } catch (Exception e) {
369                        // Ignore
370                    }
371                }
372            }
373        });
374        client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
375            public void requestSent(BOSHMessageEvent event) {
376                if (event.getBody() != null) {
377                    try {
378                        writer.write(event.getBody().toXML());
379                    } catch (Exception e) {
380                        // Ignore
381                    }
382                }
383            }
384        });
385
386        // Create and start a thread which discards all read data.
387        readerConsumer = new Thread() {
388            private Thread thread = this;
389            private int bufferLength = 1024;
390
391            public void run() {
392                try {
393                    char[] cbuf = new char[bufferLength];
394                    while (readerConsumer == thread && !done) {
395                        reader.read(cbuf, 0, bufferLength);
396                    }
397                } catch (IOException e) {
398                    // Ignore
399                }
400            }
401        };
402        readerConsumer.setDaemon(true);
403        readerConsumer.start();
404    }
405
406    /**
407     * Sends out a notification that there was an error with the connection
408     * and closes the connection.
409     *
410     * @param e the exception that causes the connection close event.
411     */
412    protected void notifyConnectionError(Exception e) {
413        // Closes the connection temporary. A reconnection is possible
414        shutdown();
415        callConnectionClosedOnErrorListener(e);
416    }
417
418    /**
419     * A listener class which listen for a successfully established connection
420     * and connection errors and notifies the BOSHConnection.
421     * 
422     * @author Guenther Niess
423     */
424    private class BOSHConnectionListener implements BOSHClientConnListener {
425
426        /**
427         * Notify the BOSHConnection about connection state changes.
428         * Process the connection listeners and try to login if the
429         * connection was formerly authenticated and is now reconnected.
430         */
431        public void connectionEvent(BOSHClientConnEvent connEvent) {
432            try {
433                if (connEvent.isConnected()) {
434                    connected = true;
435                    if (isFirstInitialization) {
436                        isFirstInitialization = false;
437                        for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
438                            listener.connectionCreated(XMPPBOSHConnection.this);
439                        }
440                    }
441                    else {
442                            if (wasAuthenticated) {
443                                try {
444                                    login();
445                                }
446                                catch (Exception e) {
447                                    throw new RuntimeException(e);
448                                }
449                            }
450                            notifyReconnection();
451                    }
452                }
453                else {
454                    if (connEvent.isError()) {
455                        // TODO Check why jbosh's getCause returns Throwable here. This is very
456                        // unusual and should be avoided if possible
457                        Throwable cause = connEvent.getCause();
458                        Exception e;
459                        if (cause instanceof Exception) {
460                            e = (Exception) cause;
461                        } else {
462                            e = new Exception(cause);
463                        }
464                        notifyConnectionError(e);
465                    }
466                    connected = false;
467                }
468            }
469            finally {
470                synchronized (XMPPBOSHConnection.this) {
471                    XMPPBOSHConnection.this.notifyAll();
472                }
473            }
474        }
475    }
476
477    /**
478     * Listens for XML traffic from the BOSH connection manager and parses it into
479     * stanza(/packet) objects.
480     *
481     * @author Guenther Niess
482     */
483    private class BOSHPacketReader implements BOSHClientResponseListener {
484
485        /**
486         * Parse the received packets and notify the corresponding connection.
487         *
488         * @param event the BOSH client response which includes the received packet.
489         */
490        public void responseReceived(BOSHMessageEvent event) {
491            AbstractBody body = event.getBody();
492            if (body != null) {
493                try {
494                    if (sessionID == null) {
495                        sessionID = body.getAttribute(BodyQName.create(XMPPBOSHConnection.BOSH_URI, "sid"));
496                    }
497                    if (streamId == null) {
498                        streamId = body.getAttribute(BodyQName.create(XMPPBOSHConnection.BOSH_URI, "authid"));
499                    }
500                    final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
501                    parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
502                    parser.setInput(new StringReader(body.toXML()));
503                    int eventType = parser.getEventType();
504                    do {
505                        eventType = parser.next();
506                        switch (eventType) {
507                        case XmlPullParser.START_TAG:
508                            String name = parser.getName();
509                            switch (name) {
510                            case Message.ELEMENT:
511                            case IQ.IQ_ELEMENT:
512                            case Presence.ELEMENT:
513                                parseAndProcessStanza(parser);
514                                break;
515                            case "challenge":
516                                // The server is challenging the SASL authentication
517                                // made by the client
518                                final String challengeData = parser.nextText();
519                                getSASLAuthentication().challengeReceived(challengeData);
520                                break;
521                            case "success":
522                                send(ComposableBody.builder().setNamespaceDefinition("xmpp",
523                                                XMPPBOSHConnection.XMPP_BOSH_NS).setAttribute(
524                                                BodyQName.createWithPrefix(XMPPBOSHConnection.XMPP_BOSH_NS, "restart",
525                                                                "xmpp"), "true").setAttribute(
526                                                BodyQName.create(XMPPBOSHConnection.BOSH_URI, "to"), getServiceName()).build());
527                                Success success = new Success(parser.nextText());
528                                getSASLAuthentication().authenticated(success);
529                                break;
530                            case "features":
531                                parseFeatures(parser);
532                                break;
533                            case "failure":
534                                if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) {
535                                    final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser);
536                                    getSASLAuthentication().authenticationFailed(failure);
537                                }
538                                break;
539                            case "error":
540                                throw new StreamErrorException(PacketParserUtils.parseStreamError(parser));
541                            }
542                            break;
543                        }
544                    }
545                    while (eventType != XmlPullParser.END_DOCUMENT);
546                }
547                catch (Exception e) {
548                    if (isConnected()) {
549                        notifyConnectionError(e);
550                    }
551                }
552            }
553        }
554    }
555}