blob: 594cf9d852a3648f408a9fcbcc474b0315f2f7d6 [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
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
21package org.jivesoftware.smack;
22
23import java.io.IOException;
24import java.io.PipedReader;
25import java.io.PipedWriter;
26import java.io.Writer;
27import java.util.concurrent.ExecutorService;
28import java.util.concurrent.Executors;
29import java.util.concurrent.ThreadFactory;
30
31import org.jivesoftware.smack.Connection;
32import org.jivesoftware.smack.ConnectionCreationListener;
33import org.jivesoftware.smack.ConnectionListener;
34import org.jivesoftware.smack.PacketCollector;
35import org.jivesoftware.smack.Roster;
36import org.jivesoftware.smack.XMPPException;
37import org.jivesoftware.smack.packet.Packet;
38import org.jivesoftware.smack.packet.Presence;
39import org.jivesoftware.smack.packet.XMPPError;
40import org.jivesoftware.smack.util.StringUtils;
41
42import com.kenai.jbosh.BOSHClient;
43import com.kenai.jbosh.BOSHClientConfig;
44import com.kenai.jbosh.BOSHClientConnEvent;
45import com.kenai.jbosh.BOSHClientConnListener;
46import com.kenai.jbosh.BOSHClientRequestListener;
47import com.kenai.jbosh.BOSHClientResponseListener;
48import com.kenai.jbosh.BOSHException;
49import com.kenai.jbosh.BOSHMessageEvent;
50import com.kenai.jbosh.BodyQName;
51import 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 */
60public 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 */
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 */
144 public BOSHConnection(BOSHConfiguration config) {
145 super(config);
146 this.config = config;
147 }
148
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
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
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
278 public String getUser() {
279 return user;
280 }
281
282 public boolean isAnonymous() {
283 return anonymous;
284 }
285
286 public boolean isAuthenticated() {
287 return authenticated;
288 }
289
290 public boolean isConnected() {
291 return connected;
292 }
293
294 public boolean isSecureConnection() {
295 // TODO: Implement SSL usage
296 return false;
297 }
298
299 public boolean isUsingCompression() {
300 // TODO: Implement compression
301 return false;
302 }
303
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
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
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
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 */
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 */
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 */
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 */
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 */
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 */
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
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 */
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
760 public ListenerNotification(Packet packet) {
761 this.packet = packet;
762 }
763
764 public void run() {
765 for (ListenerWrapper listenerWrapper : recvListeners.values()) {
766 listenerWrapper.notifyListener(packet);
767 }
768 }
769 }
770
771 @Override
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}