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 (at) 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 } 780