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