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.ConnectionException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.XMPPConnection;
033import org.jivesoftware.smack.XMPPException;
034import org.jivesoftware.smack.XMPPException.StreamErrorException;
035import org.jivesoftware.smack.packet.Element;
036import org.jivesoftware.smack.packet.IQ;
037import org.jivesoftware.smack.packet.Message;
038import org.jivesoftware.smack.packet.Nonza;
039import org.jivesoftware.smack.packet.Presence;
040import org.jivesoftware.smack.packet.Stanza;
041import org.jivesoftware.smack.packet.StanzaError;
042import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
043import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
044import org.jivesoftware.smack.util.PacketParserUtils;
045
046import org.igniterealtime.jbosh.AbstractBody;
047import org.igniterealtime.jbosh.BOSHClient;
048import org.igniterealtime.jbosh.BOSHClientConfig;
049import org.igniterealtime.jbosh.BOSHClientConnEvent;
050import org.igniterealtime.jbosh.BOSHClientConnListener;
051import org.igniterealtime.jbosh.BOSHClientRequestListener;
052import org.igniterealtime.jbosh.BOSHClientResponseListener;
053import org.igniterealtime.jbosh.BOSHException;
054import org.igniterealtime.jbosh.BOSHMessageEvent;
055import org.igniterealtime.jbosh.BodyQName;
056import org.igniterealtime.jbosh.ComposableBody;
057
058import org.jxmpp.jid.DomainBareJid;
059import org.jxmpp.jid.parts.Resourcepart;
060import org.xmlpull.v1.XmlPullParser;
061import org.xmlpull.v1.XmlPullParserFactory;
062
063/**
064 * Creates a connection to an XMPP server via HTTP binding.
065 * This is specified in the XEP-0206: XMPP Over BOSH.
066 *
067 * @see XMPPConnection
068 * @author Guenther Niess
069 */
070public class XMPPBOSHConnection extends AbstractXMPPConnection {
071    private static final Logger LOGGER = Logger.getLogger(XMPPBOSHConnection.class.getName());
072
073    /**
074     * The XMPP Over Bosh namespace.
075     */
076    public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
077
078    /**
079     * The BOSH namespace from XEP-0124.
080     */
081    public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
082
083    /**
084     * The used BOSH client from the jbosh library.
085     */
086    private BOSHClient client;
087
088    /**
089     * Holds the initial configuration used while creating the connection.
090     */
091    @SuppressWarnings("HidingField")
092    private final BOSHConfiguration config;
093
094    // Some flags which provides some info about the current state.
095    private boolean isFirstInitialization = true;
096    private boolean done = false;
097
098    // The readerPipe and consumer thread are used for the debugger.
099    private PipedWriter readerPipe;
100    private Thread readerConsumer;
101
102    /**
103     * The session ID for the BOSH session with the connection manager.
104     */
105    protected String sessionID = null;
106
107    private boolean notified;
108
109    /**
110     * Create a HTTP Binding connection to an XMPP server.
111     *
112     * @param username the username to use.
113     * @param password the password to use.
114     * @param https true if you want to use SSL
115     *             (e.g. false for http://domain.lt:7070/http-bind).
116     * @param host the hostname or IP address of the connection manager
117     *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
118     * @param port the port of the connection manager
119     *             (e.g. 7070 for http://domain.lt:7070/http-bind).
120     * @param filePath the file which is described by the URL
121     *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
122     * @param xmppServiceDomain the XMPP service name
123     *             (e.g. domain.lt for the user alice@domain.lt)
124     */
125    public XMPPBOSHConnection(String username, String password, boolean https, String host, int port, String filePath, DomainBareJid xmppServiceDomain) {
126        this(BOSHConfiguration.builder().setUseHttps(https).setHost(host)
127                .setPort(port).setFile(filePath).setXmppDomain(xmppServiceDomain)
128                .setUsernameAndPassword(username, password).build());
129    }
130
131    /**
132     * Create a HTTP Binding connection to an 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, InterruptedException {
143        done = false;
144        notified = false;
145        try {
146            // Ensure a clean starting state
147            if (client != null) {
148                client.close();
149                client = null;
150            }
151            sessionID = null;
152
153            // Initialize BOSH client
154            BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
155                    .create(config.getURI(), config.getXMPPServiceDomain().toString());
156            if (config.isProxyEnabled()) {
157                cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
158            }
159            client = BOSHClient.create(cfgBuilder.build());
160
161            client.addBOSHClientConnListener(new BOSHConnectionListener());
162            client.addBOSHClientResponseListener(new BOSHPacketReader());
163
164            // Initialize the debugger
165            if (debugger != null) {
166                initDebugger();
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                final long deadline = System.currentTimeMillis() + getReplyTimeout();
182                while (!notified) {
183                    final long now = System.currentTimeMillis();
184                    if (now >= deadline) break;
185                    wait(deadline - now);
186                }
187            }
188        }
189
190        // If there is no feedback, throw an remote server timeout error
191        if (!connected && !done) {
192            done = true;
193            String errorMessage = "Timeout reached for the connection to "
194                    + getHost() + ":" + getPort() + ".";
195            throw new SmackException(errorMessage);
196        }
197    }
198
199    @Override
200    public boolean isSecureConnection() {
201        // TODO: Implement SSL usage
202        return false;
203    }
204
205    @Override
206    public boolean isUsingCompression() {
207        // TODO: Implement compression
208        return false;
209    }
210
211    @Override
212    protected void loginInternal(String username, String password, Resourcepart resource) throws XMPPException,
213                    SmackException, IOException, InterruptedException {
214        // Authenticate using SASL
215        saslAuthentication.authenticate(username, password, config.getAuthzid(), null);
216
217        bindResourceAndEstablishSession(resource);
218
219        afterSuccessfulLogin(false);
220    }
221
222    @Override
223    public void sendNonza(Nonza element) throws NotConnectedException {
224        if (done) {
225            throw new NotConnectedException();
226        }
227        sendElement(element);
228    }
229
230    @Override
231    protected void sendStanzaInternal(Stanza packet) throws NotConnectedException {
232        sendElement(packet);
233    }
234
235    private void sendElement(Element element) {
236        try {
237            send(ComposableBody.builder().setPayloadXML(element.toXML(BOSH_URI).toString()).build());
238            if (element instanceof Stanza) {
239                firePacketSendingListeners((Stanza) element);
240            }
241        }
242        catch (BOSHException e) {
243            LOGGER.log(Level.SEVERE, "BOSHException in sendStanzaInternal", e);
244        }
245    }
246
247    /**
248     * Closes the connection by setting presence to unavailable and closing the
249     * HTTP client. The shutdown logic will be used during a planned disconnection or when
250     * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
251     * BOSH stanza reader will not be removed; thus connection's state is kept.
252     *
253     */
254    @Override
255    protected void shutdown() {
256
257        if (client != null) {
258            try {
259                client.disconnect();
260            } catch (Exception e) {
261                LOGGER.log(Level.WARNING, "shutdown", e);
262            }
263            client = null;
264        }
265
266        instantShutdown();
267    }
268
269    @Override
270    public void instantShutdown() {
271        setWasAuthenticated();
272        sessionID = null;
273        done = true;
274        authenticated = false;
275        connected = false;
276        isFirstInitialization = false;
277
278        // Close down the readers and writers.
279        if (readerPipe != null) {
280            try {
281                readerPipe.close();
282            }
283            catch (Throwable ignore) { /* ignore */ }
284            reader = null;
285        }
286        if (reader != null) {
287            try {
288                reader.close();
289            }
290            catch (Throwable ignore) { /* ignore */ }
291            reader = null;
292        }
293        if (writer != null) {
294            try {
295                writer.close();
296            }
297            catch (Throwable ignore) { /* ignore */ }
298            writer = null;
299        }
300
301        readerConsumer = null;
302    }
303
304    /**
305     * Send a HTTP request to the connection manager with the provided body element.
306     *
307     * @param body the body which will be sent.
308     * @throws BOSHException
309     */
310    protected void send(ComposableBody body) throws BOSHException {
311        if (!connected) {
312            throw new IllegalStateException("Not connected to a server!");
313        }
314        if (body == null) {
315            throw new NullPointerException("Body mustn't be null!");
316        }
317        if (sessionID != null) {
318            body = body.rebuild().setAttribute(
319                    BodyQName.create(BOSH_URI, "sid"), sessionID).build();
320        }
321        client.send(body);
322    }
323
324    /**
325     * Initialize the SmackDebugger which allows to log and debug XML traffic.
326     */
327    @Override
328    protected void initDebugger() {
329        // TODO: Maybe we want to extend the SmackDebugger for simplification
330        //       and a performance boost.
331
332        // Initialize a empty writer which discards all data.
333        writer = new Writer() {
334            @Override
335            public void write(char[] cbuf, int off, int len) {
336                /* ignore */ }
337
338            @Override
339            public void close() {
340                /* ignore */ }
341
342            @Override
343            public void flush() {
344                /* ignore */ }
345        };
346
347        // Initialize a pipe for received raw data.
348        try {
349            readerPipe = new PipedWriter();
350            reader = new PipedReader(readerPipe);
351        }
352        catch (IOException e) {
353            // Ignore
354        }
355
356        // Call the method from the parent class which initializes the debugger.
357        super.initDebugger();
358
359        // Add listeners for the received and sent raw data.
360        client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
361            @Override
362            public void responseReceived(BOSHMessageEvent event) {
363                if (event.getBody() != null) {
364                    try {
365                        readerPipe.write(event.getBody().toXML());
366                        readerPipe.flush();
367                    } catch (Exception e) {
368                        // Ignore
369                    }
370                }
371            }
372        });
373        client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
374            @Override
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            @Override
392            public void run() {
393                try {
394                    char[] cbuf = new char[bufferLength];
395                    while (readerConsumer == thread && !done) {
396                        reader.read(cbuf, 0, bufferLength);
397                    }
398                } catch (IOException e) {
399                    // Ignore
400                }
401            }
402        };
403        readerConsumer.setDaemon(true);
404        readerConsumer.start();
405    }
406
407    /**
408     * Sends out a notification that there was an error with the connection
409     * and closes the connection.
410     *
411     * @param e the exception that causes the connection close event.
412     */
413    protected void notifyConnectionError(Exception e) {
414        // Closes the connection temporary. A reconnection is possible
415        shutdown();
416        callConnectionClosedOnErrorListener(e);
417    }
418
419    /**
420     * A listener class which listen for a successfully established connection
421     * and connection errors and notifies the BOSHConnection.
422     *
423     * @author Guenther Niess
424     */
425    private class BOSHConnectionListener implements BOSHClientConnListener {
426
427        /**
428         * Notify the BOSHConnection about connection state changes.
429         * Process the connection listeners and try to login if the
430         * connection was formerly authenticated and is now reconnected.
431         */
432        @Override
433        public void connectionEvent(BOSHClientConnEvent connEvent) {
434            try {
435                if (connEvent.isConnected()) {
436                    connected = true;
437                    if (isFirstInitialization) {
438                        isFirstInitialization = false;
439                    }
440                    else {
441                            if (wasAuthenticated) {
442                                try {
443                                    login();
444                                }
445                                catch (Exception e) {
446                                    throw new RuntimeException(e);
447                                }
448                            }
449                    }
450                }
451                else {
452                    if (connEvent.isError()) {
453                        // TODO Check why jbosh's getCause returns Throwable here. This is very
454                        // unusual and should be avoided if possible
455                        Throwable cause = connEvent.getCause();
456                        Exception e;
457                        if (cause instanceof Exception) {
458                            e = (Exception) cause;
459                        } else {
460                            e = new Exception(cause);
461                        }
462                        notifyConnectionError(e);
463                    }
464                    connected = false;
465                }
466            }
467            finally {
468                notified = true;
469                synchronized (XMPPBOSHConnection.this) {
470                    XMPPBOSHConnection.this.notifyAll();
471                }
472            }
473        }
474    }
475
476    /**
477     * Listens for XML traffic from the BOSH connection manager and parses it into
478     * stanza objects.
479     *
480     * @author Guenther Niess
481     */
482    private class BOSHPacketReader implements BOSHClientResponseListener {
483
484        /**
485         * Parse the received packets and notify the corresponding connection.
486         *
487         * @param event the BOSH client response which includes the received packet.
488         */
489        @Override
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"), getXMPPServiceDomain().toString()).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                                // Some BOSH error isn't stream error.
541                                if ("urn:ietf:params:xml:ns:xmpp-streams".equals(parser.getNamespace(null))) {
542                                    throw new StreamErrorException(PacketParserUtils.parseStreamError(parser));
543                                } else {
544                                    StanzaError.Builder builder = PacketParserUtils.parseError(parser);
545                                    throw new XMPPException.XMPPErrorException(null, builder.build());
546                                }
547                            }
548                            break;
549                        }
550                    }
551                    while (eventType != XmlPullParser.END_DOCUMENT);
552                }
553                catch (Exception e) {
554                    if (isConnected()) {
555                        notifyConnectionError(e);
556                    }
557                }
558            }
559        }
560    }
561}