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