• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * $RCSfile$
3  * $Revision$
4  * $Date$
5  *
6  * Copyright 2009 Jive Software.
7  *
8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *     http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 
21 package org.jivesoftware.smack;
22 
23 import java.io.IOException;
24 import java.io.PipedReader;
25 import java.io.PipedWriter;
26 import java.io.Writer;
27 import java.util.concurrent.ExecutorService;
28 import java.util.concurrent.Executors;
29 import java.util.concurrent.ThreadFactory;
30 
31 import org.jivesoftware.smack.Connection;
32 import org.jivesoftware.smack.ConnectionCreationListener;
33 import org.jivesoftware.smack.ConnectionListener;
34 import org.jivesoftware.smack.PacketCollector;
35 import org.jivesoftware.smack.Roster;
36 import org.jivesoftware.smack.XMPPException;
37 import org.jivesoftware.smack.packet.Packet;
38 import org.jivesoftware.smack.packet.Presence;
39 import org.jivesoftware.smack.packet.XMPPError;
40 import org.jivesoftware.smack.util.StringUtils;
41 
42 import com.kenai.jbosh.BOSHClient;
43 import com.kenai.jbosh.BOSHClientConfig;
44 import com.kenai.jbosh.BOSHClientConnEvent;
45 import com.kenai.jbosh.BOSHClientConnListener;
46 import com.kenai.jbosh.BOSHClientRequestListener;
47 import com.kenai.jbosh.BOSHClientResponseListener;
48 import com.kenai.jbosh.BOSHException;
49 import com.kenai.jbosh.BOSHMessageEvent;
50 import com.kenai.jbosh.BodyQName;
51 import com.kenai.jbosh.ComposableBody;
52 
53 /**
54  * Creates a connection to a XMPP server via HTTP binding.
55  * This is specified in the XEP-0206: XMPP Over BOSH.
56  *
57  * @see Connection
58  * @author Guenther Niess
59  */
60 public class BOSHConnection extends Connection {
61 
62     /**
63      * The XMPP Over Bosh namespace.
64      */
65     public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
66 
67     /**
68      * The BOSH namespace from XEP-0124.
69      */
70     public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
71 
72     /**
73      * The used BOSH client from the jbosh library.
74      */
75     private BOSHClient client;
76 
77     /**
78      * Holds the initial configuration used while creating the connection.
79      */
80     private final BOSHConfiguration config;
81 
82     // Some flags which provides some info about the current state.
83     private boolean connected = false;
84     private boolean authenticated = false;
85     private boolean anonymous = false;
86     private boolean isFirstInitialization = true;
87     private boolean wasAuthenticated = false;
88     private boolean done = false;
89 
90     /**
91      * The Thread environment for sending packet listeners.
92      */
93     private ExecutorService listenerExecutor;
94 
95     // The readerPipe and consumer thread are used for the debugger.
96     private PipedWriter readerPipe;
97     private Thread readerConsumer;
98 
99     /**
100      * The BOSH equivalent of the stream ID which is used for DIGEST authentication.
101      */
102     protected String authID = null;
103 
104     /**
105      * The session ID for the BOSH session with the connection manager.
106      */
107     protected String sessionID = null;
108 
109     /**
110      * The full JID of the authenticated user.
111      */
112     private String user = null;
113 
114     /**
115      * The roster maybe also called buddy list holds the list of the users contacts.
116      */
117     private Roster roster = null;
118 
119 
120     /**
121      * Create a HTTP Binding connection to a XMPP server.
122      *
123      * @param https true if you want to use SSL
124      *             (e.g. false for http://domain.lt:7070/http-bind).
125      * @param host the hostname or IP address of the connection manager
126      *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
127      * @param port the port of the connection manager
128      *             (e.g. 7070 for http://domain.lt:7070/http-bind).
129      * @param filePath the file which is described by the URL
130      *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
131      * @param xmppDomain the XMPP service name
132      *             (e.g. domain.lt for the user alice@domain.lt)
133      */
BOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain)134     public BOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
135         super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
136         this.config = (BOSHConfiguration) getConfiguration();
137     }
138 
139     /**
140      * Create a HTTP Binding connection to a XMPP server.
141      *
142      * @param config The configuration which is used for this connection.
143      */
BOSHConnection(BOSHConfiguration config)144     public BOSHConnection(BOSHConfiguration config) {
145         super(config);
146         this.config = config;
147     }
148 
connect()149     public void connect() throws XMPPException {
150         if (connected) {
151             throw new IllegalStateException("Already connected to a server.");
152         }
153         done = false;
154         try {
155             // Ensure a clean starting state
156             if (client != null) {
157                 client.close();
158                 client = null;
159             }
160             saslAuthentication.init();
161             sessionID = null;
162             authID = null;
163 
164             // Initialize BOSH client
165             BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
166                     .create(config.getURI(), config.getServiceName());
167             if (config.isProxyEnabled()) {
168                 cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
169             }
170             client = BOSHClient.create(cfgBuilder.build());
171 
172             // Create an executor to deliver incoming packets to listeners.
173             // We'll use a single thread with an unbounded queue.
174             listenerExecutor = Executors
175                     .newSingleThreadExecutor(new ThreadFactory() {
176                         public Thread newThread(Runnable runnable) {
177                             Thread thread = new Thread(runnable,
178                                     "Smack Listener Processor ("
179                                             + connectionCounterValue + ")");
180                             thread.setDaemon(true);
181                             return thread;
182                         }
183                     });
184             client.addBOSHClientConnListener(new BOSHConnectionListener(this));
185             client.addBOSHClientResponseListener(new BOSHPacketReader(this));
186 
187             // Initialize the debugger
188             if (config.isDebuggerEnabled()) {
189                 initDebugger();
190                 if (isFirstInitialization) {
191                     if (debugger.getReaderListener() != null) {
192                         addPacketListener(debugger.getReaderListener(), null);
193                     }
194                     if (debugger.getWriterListener() != null) {
195                         addPacketSendingListener(debugger.getWriterListener(), null);
196                     }
197                 }
198             }
199 
200             // Send the session creation request
201             client.send(ComposableBody.builder()
202                     .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
203                     .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
204                     .build());
205         } catch (Exception e) {
206             throw new XMPPException("Can't connect to " + getServiceName(), e);
207         }
208 
209         // Wait for the response from the server
210         synchronized (this) {
211             long endTime = System.currentTimeMillis() +
212                            SmackConfiguration.getPacketReplyTimeout() * 6;
213             while ((!connected) && (System.currentTimeMillis() < endTime)) {
214                 try {
215                     wait(Math.abs(endTime - System.currentTimeMillis()));
216                 }
217                 catch (InterruptedException e) {}
218             }
219         }
220 
221         // If there is no feedback, throw an remote server timeout error
222         if (!connected && !done) {
223             done = true;
224             String errorMessage = "Timeout reached for the connection to "
225                     + getHost() + ":" + getPort() + ".";
226             throw new XMPPException(
227                     errorMessage,
228                     new XMPPError(XMPPError.Condition.remote_server_timeout, errorMessage));
229         }
230     }
231 
getConnectionID()232     public String getConnectionID() {
233         if (!connected) {
234             return null;
235         } else if (authID != null) {
236             return authID;
237         } else {
238             return sessionID;
239         }
240     }
241 
getRoster()242     public Roster getRoster() {
243         if (roster == null) {
244             return null;
245         }
246         if (!config.isRosterLoadedAtLogin()) {
247             roster.reload();
248         }
249         // If this is the first time the user has asked for the roster after calling
250         // login, we want to wait for the server to send back the user's roster.
251         // This behavior shields API users from having to worry about the fact that
252         // roster operations are asynchronous, although they'll still have to listen
253         // for changes to the roster. Note: because of this waiting logic, internal
254         // Smack code should be wary about calling the getRoster method, and may
255         // need to access the roster object directly.
256         if (!roster.rosterInitialized) {
257             try {
258                 synchronized (roster) {
259                     long waitTime = SmackConfiguration.getPacketReplyTimeout();
260                     long start = System.currentTimeMillis();
261                     while (!roster.rosterInitialized) {
262                         if (waitTime <= 0) {
263                             break;
264                         }
265                         roster.wait(waitTime);
266                         long now = System.currentTimeMillis();
267                         waitTime -= now - start;
268                         start = now;
269                     }
270                 }
271             } catch (InterruptedException ie) {
272                 // Ignore.
273             }
274         }
275         return roster;
276     }
277 
getUser()278     public String getUser() {
279         return user;
280     }
281 
isAnonymous()282     public boolean isAnonymous() {
283         return anonymous;
284     }
285 
isAuthenticated()286     public boolean isAuthenticated() {
287         return authenticated;
288     }
289 
isConnected()290     public boolean isConnected() {
291         return connected;
292     }
293 
isSecureConnection()294     public boolean isSecureConnection() {
295         // TODO: Implement SSL usage
296         return false;
297     }
298 
isUsingCompression()299     public boolean isUsingCompression() {
300         // TODO: Implement compression
301         return false;
302     }
303 
login(String username, String password, String resource)304     public void login(String username, String password, String resource)
305             throws XMPPException {
306         if (!isConnected()) {
307             throw new IllegalStateException("Not connected to server.");
308         }
309         if (authenticated) {
310             throw new IllegalStateException("Already logged in to server.");
311         }
312         // Do partial version of nameprep on the username.
313         username = username.toLowerCase().trim();
314 
315         String response;
316         if (config.isSASLAuthenticationEnabled()
317                 && saslAuthentication.hasNonAnonymousAuthentication()) {
318             // Authenticate using SASL
319             if (password != null) {
320                 response = saslAuthentication.authenticate(username, password, resource);
321             } else {
322                 response = saslAuthentication.authenticate(username, resource, config.getCallbackHandler());
323             }
324         } else {
325             // Authenticate using Non-SASL
326             response = new NonSASLAuthentication(this).authenticate(username, password, resource);
327         }
328 
329         // Set the user.
330         if (response != null) {
331             this.user = response;
332             // Update the serviceName with the one returned by the server
333             config.setServiceName(StringUtils.parseServer(response));
334         } else {
335             this.user = username + "@" + getServiceName();
336             if (resource != null) {
337                 this.user += "/" + resource;
338             }
339         }
340 
341         // Create the roster if it is not a reconnection.
342         if (this.roster == null) {
343             if (this.rosterStorage == null) {
344                 this.roster = new Roster(this);
345             } else {
346                 this.roster = new Roster(this, rosterStorage);
347             }
348         }
349 
350         // Set presence to online.
351         if (config.isSendPresence()) {
352             sendPacket(new Presence(Presence.Type.available));
353         }
354 
355         // Indicate that we're now authenticated.
356         authenticated = true;
357         anonymous = false;
358 
359         if (config.isRosterLoadedAtLogin()) {
360             this.roster.reload();
361         }
362         // Stores the autentication for future reconnection
363         config.setLoginInfo(username, password, resource);
364 
365         // If debugging is enabled, change the the debug window title to include
366         // the
367         // name we are now logged-in as.l
368         if (config.isDebuggerEnabled() && debugger != null) {
369             debugger.userHasLogged(user);
370         }
371     }
372 
loginAnonymously()373     public void loginAnonymously() throws XMPPException {
374     	if (!isConnected()) {
375             throw new IllegalStateException("Not connected to server.");
376         }
377         if (authenticated) {
378             throw new IllegalStateException("Already logged in to server.");
379         }
380 
381         String response;
382         if (config.isSASLAuthenticationEnabled() &&
383                 saslAuthentication.hasAnonymousAuthentication()) {
384             response = saslAuthentication.authenticateAnonymously();
385         }
386         else {
387             // Authenticate using Non-SASL
388             response = new NonSASLAuthentication(this).authenticateAnonymously();
389         }
390 
391         // Set the user value.
392         this.user = response;
393         // Update the serviceName with the one returned by the server
394         config.setServiceName(StringUtils.parseServer(response));
395 
396         // Anonymous users can't have a roster.
397         roster = null;
398 
399         // Set presence to online.
400         if (config.isSendPresence()) {
401             sendPacket(new Presence(Presence.Type.available));
402         }
403 
404         // Indicate that we're now authenticated.
405         authenticated = true;
406         anonymous = true;
407 
408         // If debugging is enabled, change the the debug window title to include the
409         // name we are now logged-in as.
410         // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
411         // will be null
412         if (config.isDebuggerEnabled() && debugger != null) {
413             debugger.userHasLogged(user);
414         }
415     }
416 
sendPacket(Packet packet)417     public void sendPacket(Packet packet) {
418         if (!isConnected()) {
419             throw new IllegalStateException("Not connected to server.");
420         }
421         if (packet == null) {
422             throw new NullPointerException("Packet is null.");
423         }
424         if (!done) {
425             // Invoke interceptors for the new packet that is about to be sent.
426             // Interceptors
427             // may modify the content of the packet.
428             firePacketInterceptors(packet);
429 
430             try {
431                 send(ComposableBody.builder().setPayloadXML(packet.toXML())
432                         .build());
433             } catch (BOSHException e) {
434                 e.printStackTrace();
435                 return;
436             }
437 
438             // Process packet writer listeners. Note that we're using the
439             // sending
440             // thread so it's expected that listeners are fast.
441             firePacketSendingListeners(packet);
442         }
443     }
444 
disconnect(Presence unavailablePresence)445     public void disconnect(Presence unavailablePresence) {
446         if (!connected) {
447             return;
448         }
449         shutdown(unavailablePresence);
450 
451         // Cleanup
452         if (roster != null) {
453             roster.cleanup();
454             roster = null;
455         }
456         sendListeners.clear();
457         recvListeners.clear();
458         collectors.clear();
459         interceptors.clear();
460 
461         // Reset the connection flags
462         wasAuthenticated = false;
463         isFirstInitialization = true;
464 
465         // Notify connection listeners of the connection closing if done hasn't already been set.
466         for (ConnectionListener listener : getConnectionListeners()) {
467             try {
468                 listener.connectionClosed();
469             }
470             catch (Exception e) {
471                 // Catch and print any exception so we can recover
472                 // from a faulty listener and finish the shutdown process
473                 e.printStackTrace();
474             }
475         }
476     }
477 
478     /**
479      * Closes the connection by setting presence to unavailable and closing the
480      * HTTP client. The shutdown logic will be used during a planned disconnection or when
481      * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
482      * BOSH packet reader and {@link Roster} will not be removed; thus
483      * connection's state is kept.
484      *
485      * @param unavailablePresence the presence packet to send during shutdown.
486      */
shutdown(Presence unavailablePresence)487     protected void shutdown(Presence unavailablePresence) {
488         setWasAuthenticated(authenticated);
489         authID = null;
490         sessionID = null;
491         done = true;
492         authenticated = false;
493         connected = false;
494         isFirstInitialization = false;
495 
496         try {
497             client.disconnect(ComposableBody.builder()
498                     .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
499                     .setPayloadXML(unavailablePresence.toXML())
500                     .build());
501             // Wait 150 ms for processes to clean-up, then shutdown.
502             Thread.sleep(150);
503         }
504         catch (Exception e) {
505             // Ignore.
506         }
507 
508         // Close down the readers and writers.
509         if (readerPipe != null) {
510             try {
511                 readerPipe.close();
512             }
513             catch (Throwable ignore) { /* ignore */ }
514             reader = null;
515         }
516         if (reader != null) {
517             try {
518                 reader.close();
519             }
520             catch (Throwable ignore) { /* ignore */ }
521             reader = null;
522         }
523         if (writer != null) {
524             try {
525                 writer.close();
526             }
527             catch (Throwable ignore) { /* ignore */ }
528             writer = null;
529         }
530 
531         // Shut down the listener executor.
532         if (listenerExecutor != null) {
533             listenerExecutor.shutdown();
534         }
535         readerConsumer = null;
536     }
537 
538     /**
539      * Sets whether the connection has already logged in the server.
540      *
541      * @param wasAuthenticated true if the connection has already been authenticated.
542      */
setWasAuthenticated(boolean wasAuthenticated)543     private void setWasAuthenticated(boolean wasAuthenticated) {
544         if (!this.wasAuthenticated) {
545             this.wasAuthenticated = wasAuthenticated;
546         }
547     }
548 
549     /**
550      * Send a HTTP request to the connection manager with the provided body element.
551      *
552      * @param body the body which will be sent.
553      */
send(ComposableBody body)554     protected void send(ComposableBody body) throws BOSHException {
555         if (!connected) {
556             throw new IllegalStateException("Not connected to a server!");
557         }
558         if (body == null) {
559             throw new NullPointerException("Body mustn't be null!");
560         }
561         if (sessionID != null) {
562             body = body.rebuild().setAttribute(
563                     BodyQName.create(BOSH_URI, "sid"), sessionID).build();
564         }
565         client.send(body);
566     }
567 
568     /**
569      * Processes a packet after it's been fully parsed by looping through the
570      * installed packet collectors and listeners and letting them examine the
571      * packet to see if they are a match with the filter.
572      *
573      * @param packet the packet to process.
574      */
processPacket(Packet packet)575     protected void processPacket(Packet packet) {
576         if (packet == null) {
577             return;
578         }
579 
580         // Loop through all collectors and notify the appropriate ones.
581         for (PacketCollector collector : getPacketCollectors()) {
582             collector.processPacket(packet);
583         }
584 
585         // Deliver the incoming packet to listeners.
586         listenerExecutor.submit(new ListenerNotification(packet));
587     }
588 
589     /**
590      * Initialize the SmackDebugger which allows to log and debug XML traffic.
591      */
initDebugger()592     protected void initDebugger() {
593         // TODO: Maybe we want to extend the SmackDebugger for simplification
594         //       and a performance boost.
595 
596         // Initialize a empty writer which discards all data.
597         writer = new Writer() {
598                 public void write(char[] cbuf, int off, int len) { /* ignore */}
599                 public void close() { /* ignore */ }
600                 public void flush() { /* ignore */ }
601             };
602 
603         // Initialize a pipe for received raw data.
604         try {
605             readerPipe = new PipedWriter();
606             reader = new PipedReader(readerPipe);
607         }
608         catch (IOException e) {
609             // Ignore
610         }
611 
612         // Call the method from the parent class which initializes the debugger.
613         super.initDebugger();
614 
615         // Add listeners for the received and sent raw data.
616         client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
617             public void responseReceived(BOSHMessageEvent event) {
618                 if (event.getBody() != null) {
619                     try {
620                         readerPipe.write(event.getBody().toXML());
621                         readerPipe.flush();
622                     } catch (Exception e) {
623                         // Ignore
624                     }
625                 }
626             }
627         });
628         client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
629             public void requestSent(BOSHMessageEvent event) {
630                 if (event.getBody() != null) {
631                     try {
632                         writer.write(event.getBody().toXML());
633                     } catch (Exception e) {
634                         // Ignore
635                     }
636                 }
637             }
638         });
639 
640         // Create and start a thread which discards all read data.
641         readerConsumer = new Thread() {
642             private Thread thread = this;
643             private int bufferLength = 1024;
644 
645             public void run() {
646                 try {
647                     char[] cbuf = new char[bufferLength];
648                     while (readerConsumer == thread && !done) {
649                         reader.read(cbuf, 0, bufferLength);
650                     }
651                 } catch (IOException e) {
652                     // Ignore
653                 }
654             }
655         };
656         readerConsumer.setDaemon(true);
657         readerConsumer.start();
658     }
659 
660     /**
661      * Sends out a notification that there was an error with the connection
662      * and closes the connection.
663      *
664      * @param e the exception that causes the connection close event.
665      */
notifyConnectionError(Exception e)666     protected void notifyConnectionError(Exception e) {
667         // Closes the connection temporary. A reconnection is possible
668         shutdown(new Presence(Presence.Type.unavailable));
669         // Print the stack trace to help catch the problem
670         e.printStackTrace();
671         // Notify connection listeners of the error.
672         for (ConnectionListener listener : getConnectionListeners()) {
673             try {
674                 listener.connectionClosedOnError(e);
675             }
676             catch (Exception e2) {
677                 // Catch and print any exception so we can recover
678                 // from a faulty listener
679                 e2.printStackTrace();
680             }
681         }
682     }
683 
684 
685     /**
686      * A listener class which listen for a successfully established connection
687      * and connection errors and notifies the BOSHConnection.
688      *
689      * @author Guenther Niess
690      */
691     private class BOSHConnectionListener implements BOSHClientConnListener {
692 
693         private final BOSHConnection connection;
694 
BOSHConnectionListener(BOSHConnection connection)695         public BOSHConnectionListener(BOSHConnection connection) {
696             this.connection = connection;
697         }
698 
699         /**
700          * Notify the BOSHConnection about connection state changes.
701          * Process the connection listeners and try to login if the
702          * connection was formerly authenticated and is now reconnected.
703          */
connectionEvent(BOSHClientConnEvent connEvent)704         public void connectionEvent(BOSHClientConnEvent connEvent) {
705             try {
706                 if (connEvent.isConnected()) {
707                     connected = true;
708                     if (isFirstInitialization) {
709                         isFirstInitialization = false;
710                         for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
711                             listener.connectionCreated(connection);
712                         }
713                     }
714                     else {
715                         try {
716                             if (wasAuthenticated) {
717                                 connection.login(
718                                         config.getUsername(),
719                                         config.getPassword(),
720                                         config.getResource());
721                             }
722                             for (ConnectionListener listener : getConnectionListeners()) {
723                                  listener.reconnectionSuccessful();
724                             }
725                         }
726                         catch (XMPPException e) {
727                             for (ConnectionListener listener : getConnectionListeners()) {
728                                 listener.reconnectionFailed(e);
729                            }
730                         }
731                     }
732                 }
733                 else {
734                     if (connEvent.isError()) {
735                         try {
736                             connEvent.getCause();
737                         }
738                         catch (Exception e) {
739                             notifyConnectionError(e);
740                         }
741                     }
742                     connected = false;
743                 }
744             }
745             finally {
746                 synchronized (connection) {
747                     connection.notifyAll();
748                 }
749             }
750         }
751     }
752 
753     /**
754      * This class notifies all listeners that a packet was received.
755      */
756     private class ListenerNotification implements Runnable {
757 
758         private Packet packet;
759 
ListenerNotification(Packet packet)760         public ListenerNotification(Packet packet) {
761             this.packet = packet;
762         }
763 
run()764         public void run() {
765             for (ListenerWrapper listenerWrapper : recvListeners.values()) {
766                 listenerWrapper.notifyListener(packet);
767             }
768         }
769     }
770 
771 	@Override
setRosterStorage(RosterStorage storage)772 	public void setRosterStorage(RosterStorage storage)
773 			throws IllegalStateException {
774 		if(this.roster!=null){
775 			throw new IllegalStateException("Roster is already initialized");
776 		}
777 		this.rosterStorage = storage;
778 	}
779 }
780