1 /** 2 * $RCSfile$ 3 * $Revision$ 4 * $Date$ 5 * 6 * Copyright 2003-2007 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 org.jivesoftware.smack.compression.XMPPInputOutputStream; 24 import org.jivesoftware.smack.filter.PacketFilter; 25 import org.jivesoftware.smack.packet.Packet; 26 import org.jivesoftware.smack.packet.Presence; 27 import org.jivesoftware.smack.packet.XMPPError; 28 import org.jivesoftware.smack.util.StringUtils; 29 import org.jivesoftware.smack.util.dns.HostAddress; 30 31 import javax.net.ssl.KeyManager; 32 import javax.net.ssl.KeyManagerFactory; 33 import javax.net.ssl.SSLContext; 34 import javax.net.ssl.SSLSocket; 35 import org.apache.harmony.javax.security.auth.callback.Callback; 36 import org.apache.harmony.javax.security.auth.callback.CallbackHandler; 37 import org.apache.harmony.javax.security.auth.callback.PasswordCallback; 38 39 import java.io.BufferedReader; 40 import java.io.BufferedWriter; 41 import java.io.ByteArrayInputStream; 42 import java.io.FileInputStream; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.io.InputStreamReader; 46 import java.io.OutputStream; 47 import java.io.OutputStreamWriter; 48 import java.lang.reflect.Constructor; 49 import java.net.Socket; 50 import java.net.UnknownHostException; 51 import java.security.KeyStore; 52 import java.security.Provider; 53 import java.security.Security; 54 import java.util.Collection; 55 import java.util.Iterator; 56 import java.util.LinkedList; 57 import java.util.List; 58 59 /** 60 * Creates a socket connection to a XMPP server. This is the default connection 61 * to a Jabber server and is specified in the XMPP Core (RFC 3920). 62 * 63 * @see Connection 64 * @author Matt Tucker 65 */ 66 public class XMPPConnection extends Connection { 67 68 /** 69 * The socket which is used for this connection. 70 */ 71 Socket socket; 72 73 String connectionID = null; 74 private String user = null; 75 private boolean connected = false; 76 // socketClosed is used concurrent 77 // by XMPPConnection, PacketReader, PacketWriter 78 private volatile boolean socketClosed = false; 79 80 /** 81 * Flag that indicates if the user is currently authenticated with the server. 82 */ 83 private boolean authenticated = false; 84 /** 85 * Flag that indicates if the user was authenticated with the server when the connection 86 * to the server was closed (abruptly or not). 87 */ 88 private boolean wasAuthenticated = false; 89 private boolean anonymous = false; 90 private boolean usingTLS = false; 91 92 PacketWriter packetWriter; 93 PacketReader packetReader; 94 95 Roster roster = null; 96 97 /** 98 * Collection of available stream compression methods offered by the server. 99 */ 100 private Collection<String> compressionMethods; 101 102 /** 103 * Set to true by packet writer if the server acknowledged the compression 104 */ 105 private boolean serverAckdCompression = false; 106 107 /** 108 * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be 109 * performed to determine the IP address and port corresponding to the 110 * service name; if that lookup fails, it's assumed that server resides at 111 * <tt>serviceName</tt> with the default port of 5222. Encrypted connections (TLS) 112 * will be used if available, stream compression is disabled, and standard SASL 113 * mechanisms will be used for authentication.<p> 114 * <p/> 115 * This is the simplest constructor for connecting to an XMPP server. Alternatively, 116 * you can get fine-grained control over connection settings using the 117 * {@link #XMPPConnection(ConnectionConfiguration)} constructor.<p> 118 * <p/> 119 * Note that XMPPConnection constructors do not establish a connection to the server 120 * and you must call {@link #connect()}.<p> 121 * <p/> 122 * The CallbackHandler will only be used if the connection requires the client provide 123 * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback 124 * to prompt for a password to unlock the keystore containing the SSL certificate. 125 * 126 * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>. 127 * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore. 128 */ 129 public XMPPConnection(String serviceName, CallbackHandler callbackHandler) { 130 // Create the configuration for this new connection 131 super(new ConnectionConfiguration(serviceName)); 132 config.setCompressionEnabled(false); 133 config.setSASLAuthenticationEnabled(true); 134 config.setDebuggerEnabled(DEBUG_ENABLED); 135 config.setCallbackHandler(callbackHandler); 136 } 137 138 /** 139 * Creates a new XMPP connection in the same way {@link #XMPPConnection(String,CallbackHandler)} does, but 140 * with no callback handler for password prompting of the keystore. This will work 141 * in most cases, provided the client is not required to provide a certificate to 142 * the server. 143 * 144 * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>. 145 */ 146 public XMPPConnection(String serviceName) { 147 // Create the configuration for this new connection 148 super(new ConnectionConfiguration(serviceName)); 149 config.setCompressionEnabled(false); 150 config.setSASLAuthenticationEnabled(true); 151 config.setDebuggerEnabled(DEBUG_ENABLED); 152 } 153 154 /** 155 * Creates a new XMPP connection in the same way {@link #XMPPConnection(ConnectionConfiguration,CallbackHandler)} does, but 156 * with no callback handler for password prompting of the keystore. This will work 157 * in most cases, provided the client is not required to provide a certificate to 158 * the server. 159 * 160 * 161 * @param config the connection configuration. 162 */ 163 public XMPPConnection(ConnectionConfiguration config) { 164 super(config); 165 } 166 167 /** 168 * Creates a new XMPP connection using the specified connection configuration.<p> 169 * <p/> 170 * Manually specifying connection configuration information is suitable for 171 * advanced users of the API. In many cases, using the 172 * {@link #XMPPConnection(String)} constructor is a better approach.<p> 173 * <p/> 174 * Note that XMPPConnection constructors do not establish a connection to the server 175 * and you must call {@link #connect()}.<p> 176 * <p/> 177 * 178 * The CallbackHandler will only be used if the connection requires the client provide 179 * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback 180 * to prompt for a password to unlock the keystore containing the SSL certificate. 181 * 182 * @param config the connection configuration. 183 * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore. 184 */ 185 public XMPPConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) { 186 super(config); 187 config.setCallbackHandler(callbackHandler); 188 } 189 190 public String getConnectionID() { 191 if (!isConnected()) { 192 return null; 193 } 194 return connectionID; 195 } 196 197 public String getUser() { 198 if (!isAuthenticated()) { 199 return null; 200 } 201 return user; 202 } 203 204 @Override 205 public synchronized void login(String username, String password, String resource) throws XMPPException { 206 if (!isConnected()) { 207 throw new IllegalStateException("Not connected to server."); 208 } 209 if (authenticated) { 210 throw new IllegalStateException("Already logged in to server."); 211 } 212 // Do partial version of nameprep on the username. 213 username = username.toLowerCase().trim(); 214 215 String response; 216 if (config.isSASLAuthenticationEnabled() && 217 saslAuthentication.hasNonAnonymousAuthentication()) { 218 // Authenticate using SASL 219 if (password != null) { 220 response = saslAuthentication.authenticate(username, password, resource); 221 } 222 else { 223 response = saslAuthentication 224 .authenticate(username, resource, config.getCallbackHandler()); 225 } 226 } 227 else { 228 // Authenticate using Non-SASL 229 response = new NonSASLAuthentication(this).authenticate(username, password, resource); 230 } 231 232 // Set the user. 233 if (response != null) { 234 this.user = response; 235 // Update the serviceName with the one returned by the server 236 config.setServiceName(StringUtils.parseServer(response)); 237 } 238 else { 239 this.user = username + "@" + getServiceName(); 240 if (resource != null) { 241 this.user += "/" + resource; 242 } 243 } 244 245 // If compression is enabled then request the server to use stream compression 246 if (config.isCompressionEnabled()) { 247 useCompression(); 248 } 249 250 // Indicate that we're now authenticated. 251 authenticated = true; 252 anonymous = false; 253 254 // Create the roster if it is not a reconnection or roster already created by getRoster() 255 if (this.roster == null) { 256 if(rosterStorage==null){ 257 this.roster = new Roster(this); 258 } 259 else{ 260 this.roster = new Roster(this,rosterStorage); 261 } 262 } 263 if (config.isRosterLoadedAtLogin()) { 264 this.roster.reload(); 265 } 266 267 // Set presence to online. 268 if (config.isSendPresence()) { 269 packetWriter.sendPacket(new Presence(Presence.Type.available)); 270 } 271 272 // Stores the authentication for future reconnection 273 config.setLoginInfo(username, password, resource); 274 275 // If debugging is enabled, change the the debug window title to include the 276 // name we are now logged-in as. 277 // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger 278 // will be null 279 if (config.isDebuggerEnabled() && debugger != null) { 280 debugger.userHasLogged(user); 281 } 282 } 283 284 @Override 285 public synchronized void loginAnonymously() throws XMPPException { 286 if (!isConnected()) { 287 throw new IllegalStateException("Not connected to server."); 288 } 289 if (authenticated) { 290 throw new IllegalStateException("Already logged in to server."); 291 } 292 293 String response; 294 if (config.isSASLAuthenticationEnabled() && 295 saslAuthentication.hasAnonymousAuthentication()) { 296 response = saslAuthentication.authenticateAnonymously(); 297 } 298 else { 299 // Authenticate using Non-SASL 300 response = new NonSASLAuthentication(this).authenticateAnonymously(); 301 } 302 303 // Set the user value. 304 this.user = response; 305 // Update the serviceName with the one returned by the server 306 config.setServiceName(StringUtils.parseServer(response)); 307 308 // If compression is enabled then request the server to use stream compression 309 if (config.isCompressionEnabled()) { 310 useCompression(); 311 } 312 313 // Set presence to online. 314 packetWriter.sendPacket(new Presence(Presence.Type.available)); 315 316 // Indicate that we're now authenticated. 317 authenticated = true; 318 anonymous = true; 319 320 // If debugging is enabled, change the the debug window title to include the 321 // name we are now logged-in as. 322 // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger 323 // will be null 324 if (config.isDebuggerEnabled() && debugger != null) { 325 debugger.userHasLogged(user); 326 } 327 } 328 329 public Roster getRoster() { 330 // synchronize against login() 331 synchronized(this) { 332 // if connection is authenticated the roster is already set by login() 333 // or a previous call to getRoster() 334 if (!isAuthenticated() || isAnonymous()) { 335 if (roster == null) { 336 roster = new Roster(this); 337 } 338 return roster; 339 } 340 } 341 342 if (!config.isRosterLoadedAtLogin()) { 343 roster.reload(); 344 } 345 // If this is the first time the user has asked for the roster after calling 346 // login, we want to wait for the server to send back the user's roster. This 347 // behavior shields API users from having to worry about the fact that roster 348 // operations are asynchronous, although they'll still have to listen for 349 // changes to the roster. Note: because of this waiting logic, internal 350 // Smack code should be wary about calling the getRoster method, and may need to 351 // access the roster object directly. 352 if (!roster.rosterInitialized) { 353 try { 354 synchronized (roster) { 355 long waitTime = SmackConfiguration.getPacketReplyTimeout(); 356 long start = System.currentTimeMillis(); 357 while (!roster.rosterInitialized) { 358 if (waitTime <= 0) { 359 break; 360 } 361 roster.wait(waitTime); 362 long now = System.currentTimeMillis(); 363 waitTime -= now - start; 364 start = now; 365 } 366 } 367 } 368 catch (InterruptedException ie) { 369 // Ignore. 370 } 371 } 372 return roster; 373 } 374 375 public boolean isConnected() { 376 return connected; 377 } 378 379 public boolean isSecureConnection() { 380 return isUsingTLS(); 381 } 382 383 public boolean isSocketClosed() { 384 return socketClosed; 385 } 386 387 public boolean isAuthenticated() { 388 return authenticated; 389 } 390 391 public boolean isAnonymous() { 392 return anonymous; 393 } 394 395 /** 396 * Closes the connection by setting presence to unavailable then closing the stream to 397 * the XMPP server. The shutdown logic will be used during a planned disconnection or when 398 * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's 399 * packet reader, packet writer, and {@link Roster} will not be removed; thus 400 * connection's state is kept. 401 * 402 * @param unavailablePresence the presence packet to send during shutdown. 403 */ 404 protected void shutdown(Presence unavailablePresence) { 405 // Set presence to offline. 406 if (packetWriter != null) { 407 packetWriter.sendPacket(unavailablePresence); 408 } 409 410 this.setWasAuthenticated(authenticated); 411 authenticated = false; 412 413 if (packetReader != null) { 414 packetReader.shutdown(); 415 } 416 if (packetWriter != null) { 417 packetWriter.shutdown(); 418 } 419 420 // Wait 150 ms for processes to clean-up, then shutdown. 421 try { 422 Thread.sleep(150); 423 } 424 catch (Exception e) { 425 // Ignore. 426 } 427 428 // Set socketClosed to true. This will cause the PacketReader 429 // and PacketWriter to ingore any Exceptions that are thrown 430 // because of a read/write from/to a closed stream. 431 // It is *important* that this is done before socket.close()! 432 socketClosed = true; 433 try { 434 socket.close(); 435 } catch (Exception e) { 436 e.printStackTrace(); 437 } 438 // In most cases the close() should be successful, so set 439 // connected to false here. 440 connected = false; 441 442 // Close down the readers and writers. 443 if (reader != null) { 444 try { 445 // Should already be closed by the previous 446 // socket.close(). But just in case do it explicitly. 447 reader.close(); 448 } 449 catch (Throwable ignore) { /* ignore */ } 450 reader = null; 451 } 452 if (writer != null) { 453 try { 454 // Should already be closed by the previous 455 // socket.close(). But just in case do it explicitly. 456 writer.close(); 457 } 458 catch (Throwable ignore) { /* ignore */ } 459 writer = null; 460 } 461 462 // Make sure that the socket is really closed 463 try { 464 // Does nothing if the socket is already closed 465 socket.close(); 466 } 467 catch (Exception e) { 468 // Ignore. 469 } 470 471 saslAuthentication.init(); 472 } 473 474 public synchronized void disconnect(Presence unavailablePresence) { 475 // If not connected, ignore this request. 476 PacketReader packetReader = this.packetReader; 477 PacketWriter packetWriter = this.packetWriter; 478 if (packetReader == null || packetWriter == null) { 479 return; 480 } 481 482 if (!isConnected()) { 483 return; 484 } 485 486 shutdown(unavailablePresence); 487 488 if (roster != null) { 489 roster.cleanup(); 490 roster = null; 491 } 492 chatManager = null; 493 494 wasAuthenticated = false; 495 496 packetWriter.cleanup(); 497 packetReader.cleanup(); 498 } 499 500 public void sendPacket(Packet packet) { 501 if (!isConnected()) { 502 throw new IllegalStateException("Not connected to server."); 503 } 504 if (packet == null) { 505 throw new NullPointerException("Packet is null."); 506 } 507 packetWriter.sendPacket(packet); 508 } 509 510 /** 511 * Registers a packet interceptor with this connection. The interceptor will be 512 * invoked every time a packet is about to be sent by this connection. Interceptors 513 * may modify the packet to be sent. A packet filter determines which packets 514 * will be delivered to the interceptor. 515 * 516 * @param packetInterceptor the packet interceptor to notify of packets about to be sent. 517 * @param packetFilter the packet filter to use. 518 * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}. 519 */ 520 public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor, 521 PacketFilter packetFilter) { 522 addPacketInterceptor(packetInterceptor, packetFilter); 523 } 524 525 /** 526 * Removes a packet interceptor. 527 * 528 * @param packetInterceptor the packet interceptor to remove. 529 * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}. 530 */ 531 public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) { 532 removePacketInterceptor(packetInterceptor); 533 } 534 535 /** 536 * Registers a packet listener with this connection. The listener will be 537 * notified of every packet that this connection sends. A packet filter determines 538 * which packets will be delivered to the listener. Note that the thread 539 * that writes packets will be used to invoke the listeners. Therefore, each 540 * packet listener should complete all operations quickly or use a different 541 * thread for processing. 542 * 543 * @param packetListener the packet listener to notify of sent packets. 544 * @param packetFilter the packet filter to use. 545 * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}. 546 */ 547 public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) { 548 addPacketSendingListener(packetListener, packetFilter); 549 } 550 551 /** 552 * Removes a packet listener for sending packets from this connection. 553 * 554 * @param packetListener the packet listener to remove. 555 * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}. 556 */ 557 public void removePacketWriterListener(PacketListener packetListener) { 558 removePacketSendingListener(packetListener); 559 } 560 561 private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { 562 XMPPException exception = null; 563 Iterator<HostAddress> it = config.getHostAddresses().iterator(); 564 List<HostAddress> failedAddresses = new LinkedList<HostAddress>(); 565 boolean xmppIOError = false; 566 while (it.hasNext()) { 567 exception = null; 568 HostAddress hostAddress = it.next(); 569 String host = hostAddress.getFQDN(); 570 int port = hostAddress.getPort(); 571 try { 572 if (config.getSocketFactory() == null) { 573 this.socket = new Socket(host, port); 574 } 575 else { 576 this.socket = config.getSocketFactory().createSocket(host, port); 577 } 578 } catch (UnknownHostException uhe) { 579 String errorMessage = "Could not connect to " + host + ":" + port + "."; 580 exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout, 581 errorMessage), uhe); 582 } catch (IOException ioe) { 583 String errorMessage = "XMPPError connecting to " + host + ":" + port + "."; 584 exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error, 585 errorMessage), ioe); 586 xmppIOError = true; 587 } 588 if (exception == null) { 589 // We found a host to connect to, break here 590 config.setUsedHostAddress(hostAddress); 591 break; 592 } 593 hostAddress.setException(exception); 594 failedAddresses.add(hostAddress); 595 if (!it.hasNext()) { 596 // There are no more host addresses to try 597 // throw an exception and report all tried 598 // HostAddresses in the exception 599 StringBuilder sb = new StringBuilder(); 600 for (HostAddress fha : failedAddresses) { 601 sb.append(fha.getErrorMessage()); 602 sb.append("; "); 603 } 604 XMPPError xmppError; 605 if (xmppIOError) { 606 xmppError = new XMPPError(XMPPError.Condition.remote_server_error); 607 } 608 else { 609 xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout); 610 } 611 throw new XMPPException(sb.toString(), xmppError); 612 } 613 } 614 socketClosed = false; 615 initConnection(); 616 } 617 618 /** 619 * Initializes the connection by creating a packet reader and writer and opening a 620 * XMPP stream to the server. 621 * 622 * @throws XMPPException if establishing a connection to the server fails. 623 */ 624 private void initConnection() throws XMPPException { 625 boolean isFirstInitialization = packetReader == null || packetWriter == null; 626 compressionHandler = null; 627 serverAckdCompression = false; 628 629 // Set the reader and writer instance variables 630 initReaderAndWriter(); 631 632 try { 633 if (isFirstInitialization) { 634 packetWriter = new PacketWriter(this); 635 packetReader = new PacketReader(this); 636 637 // If debugging is enabled, we should start the thread that will listen for 638 // all packets and then log them. 639 if (config.isDebuggerEnabled()) { 640 addPacketListener(debugger.getReaderListener(), null); 641 if (debugger.getWriterListener() != null) { 642 addPacketSendingListener(debugger.getWriterListener(), null); 643 } 644 } 645 } 646 else { 647 packetWriter.init(); 648 packetReader.init(); 649 } 650 651 // Start the packet writer. This will open a XMPP stream to the server 652 packetWriter.startup(); 653 // Start the packet reader. The startup() method will block until we 654 // get an opening stream packet back from server. 655 packetReader.startup(); 656 657 // Make note of the fact that we're now connected. 658 connected = true; 659 660 if (isFirstInitialization) { 661 // Notify listeners that a new connection has been established 662 for (ConnectionCreationListener listener : getConnectionCreationListeners()) { 663 listener.connectionCreated(this); 664 } 665 } 666 else if (!wasAuthenticated) { 667 notifyReconnection(); 668 } 669 670 } 671 catch (XMPPException ex) { 672 // An exception occurred in setting up the connection. Make sure we shut down the 673 // readers and writers and close the socket. 674 675 if (packetWriter != null) { 676 try { 677 packetWriter.shutdown(); 678 } 679 catch (Throwable ignore) { /* ignore */ } 680 packetWriter = null; 681 } 682 if (packetReader != null) { 683 try { 684 packetReader.shutdown(); 685 } 686 catch (Throwable ignore) { /* ignore */ } 687 packetReader = null; 688 } 689 if (reader != null) { 690 try { 691 reader.close(); 692 } 693 catch (Throwable ignore) { /* ignore */ } 694 reader = null; 695 } 696 if (writer != null) { 697 try { 698 writer.close(); 699 } 700 catch (Throwable ignore) { /* ignore */} 701 writer = null; 702 } 703 if (socket != null) { 704 try { 705 socket.close(); 706 } 707 catch (Exception e) { /* ignore */ } 708 socket = null; 709 } 710 this.setWasAuthenticated(authenticated); 711 chatManager = null; 712 authenticated = false; 713 connected = false; 714 715 throw ex; // Everything stoppped. Now throw the exception. 716 } 717 } 718 719 private void initReaderAndWriter() throws XMPPException { 720 try { 721 if (compressionHandler == null) { 722 reader = 723 new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); 724 writer = new BufferedWriter( 725 new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); 726 } 727 else { 728 try { 729 OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream()); 730 writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); 731 732 InputStream is = compressionHandler.getInputStream(socket.getInputStream()); 733 reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); 734 } 735 catch (Exception e) { 736 e.printStackTrace(); 737 compressionHandler = null; 738 reader = new BufferedReader( 739 new InputStreamReader(socket.getInputStream(), "UTF-8")); 740 writer = new BufferedWriter( 741 new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); 742 } 743 } 744 } 745 catch (IOException ioe) { 746 throw new XMPPException( 747 "XMPPError establishing connection with server.", 748 new XMPPError(XMPPError.Condition.remote_server_error, 749 "XMPPError establishing connection with server."), 750 ioe); 751 } 752 753 // If debugging is enabled, we open a window and write out all network traffic. 754 initDebugger(); 755 } 756 757 /*********************************************** 758 * TLS code below 759 **********************************************/ 760 761 /** 762 * Returns true if the connection to the server has successfully negotiated TLS. Once TLS 763 * has been negotiatied the connection has been secured. 764 * 765 * @return true if the connection to the server has successfully negotiated TLS. 766 */ 767 public boolean isUsingTLS() { 768 return usingTLS; 769 } 770 771 /** 772 * Notification message saying that the server supports TLS so confirm the server that we 773 * want to secure the connection. 774 * 775 * @param required true when the server indicates that TLS is required. 776 */ 777 void startTLSReceived(boolean required) { 778 if (required && config.getSecurityMode() == 779 ConnectionConfiguration.SecurityMode.disabled) { 780 notifyConnectionError(new IllegalStateException( 781 "TLS required by server but not allowed by connection configuration")); 782 return; 783 } 784 785 if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) { 786 // Do not secure the connection using TLS since TLS was disabled 787 return; 788 } 789 try { 790 writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); 791 writer.flush(); 792 } 793 catch (IOException e) { 794 notifyConnectionError(e); 795 } 796 } 797 798 /** 799 * The server has indicated that TLS negotiation can start. We now need to secure the 800 * existing plain connection and perform a handshake. This method won't return until the 801 * connection has finished the handshake or an error occured while securing the connection. 802 * 803 * @throws Exception if an exception occurs. 804 */ 805 void proceedTLSReceived() throws Exception { 806 SSLContext context = this.config.getCustomSSLContext(); 807 KeyStore ks = null; 808 KeyManager[] kms = null; 809 PasswordCallback pcb = null; 810 811 if(config.getCallbackHandler() == null) { 812 ks = null; 813 } else if (context == null) { 814 //System.out.println("Keystore type: "+configuration.getKeystoreType()); 815 if(config.getKeystoreType().equals("NONE")) { 816 ks = null; 817 pcb = null; 818 } 819 else if(config.getKeystoreType().equals("PKCS11")) { 820 try { 821 Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class); 822 String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library(); 823 ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes()); 824 Provider p = (Provider)c.newInstance(config); 825 Security.addProvider(p); 826 ks = KeyStore.getInstance("PKCS11",p); 827 pcb = new PasswordCallback("PKCS11 Password: ",false); 828 this.config.getCallbackHandler().handle(new Callback[]{pcb}); 829 ks.load(null,pcb.getPassword()); 830 } 831 catch (Exception e) { 832 ks = null; 833 pcb = null; 834 } 835 } 836 else if(config.getKeystoreType().equals("Apple")) { 837 ks = KeyStore.getInstance("KeychainStore","Apple"); 838 ks.load(null,null); 839 //pcb = new PasswordCallback("Apple Keychain",false); 840 //pcb.setPassword(null); 841 } 842 else { 843 ks = KeyStore.getInstance(config.getKeystoreType()); 844 try { 845 pcb = new PasswordCallback("Keystore Password: ",false); 846 config.getCallbackHandler().handle(new Callback[]{pcb}); 847 ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword()); 848 } 849 catch(Exception e) { 850 ks = null; 851 pcb = null; 852 } 853 } 854 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 855 try { 856 if(pcb == null) { 857 kmf.init(ks,null); 858 } else { 859 kmf.init(ks,pcb.getPassword()); 860 pcb.clearPassword(); 861 } 862 kms = kmf.getKeyManagers(); 863 } catch (NullPointerException npe) { 864 kms = null; 865 } 866 } 867 868 // Verify certificate presented by the server 869 if (context == null) { 870 context = SSLContext.getInstance("TLS"); 871 context.init(kms, new javax.net.ssl.TrustManager[] { new ServerTrustManager(getServiceName(), config) }, 872 new java.security.SecureRandom()); 873 } 874 Socket plain = socket; 875 // Secure the plain connection 876 socket = context.getSocketFactory().createSocket(plain, 877 plain.getInetAddress().getHostAddress(), plain.getPort(), true); 878 socket.setSoTimeout(0); 879 socket.setKeepAlive(true); 880 // Initialize the reader and writer with the new secured version 881 initReaderAndWriter(); 882 // Proceed to do the handshake 883 ((SSLSocket) socket).startHandshake(); 884 //if (((SSLSocket) socket).getWantClientAuth()) { 885 // System.err.println("Connection wants client auth"); 886 //} 887 //else if (((SSLSocket) socket).getNeedClientAuth()) { 888 // System.err.println("Connection needs client auth"); 889 //} 890 //else { 891 // System.err.println("Connection does not require client auth"); 892 // } 893 // Set that TLS was successful 894 usingTLS = true; 895 896 // Set the new writer to use 897 packetWriter.setWriter(writer); 898 // Send a new opening stream to the server 899 packetWriter.openStream(); 900 } 901 902 /** 903 * Sets the available stream compression methods offered by the server. 904 * 905 * @param methods compression methods offered by the server. 906 */ 907 void setAvailableCompressionMethods(Collection<String> methods) { 908 compressionMethods = methods; 909 } 910 911 /** 912 * Returns the compression handler that can be used for one compression methods offered by the server. 913 * 914 * @return a instance of XMPPInputOutputStream or null if no suitable instance was found 915 * 916 */ 917 private XMPPInputOutputStream maybeGetCompressionHandler() { 918 if (compressionMethods != null) { 919 for (XMPPInputOutputStream handler : compressionHandlers) { 920 if (!handler.isSupported()) 921 continue; 922 923 String method = handler.getCompressionMethod(); 924 if (compressionMethods.contains(method)) 925 return handler; 926 } 927 } 928 return null; 929 } 930 931 public boolean isUsingCompression() { 932 return compressionHandler != null && serverAckdCompression; 933 } 934 935 /** 936 * Starts using stream compression that will compress network traffic. Traffic can be 937 * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network 938 * connection. However, the server and the client will need to use more CPU time in order to 939 * un/compress network data so under high load the server performance might be affected.<p> 940 * <p/> 941 * Stream compression has to have been previously offered by the server. Currently only the 942 * zlib method is supported by the client. Stream compression negotiation has to be done 943 * before authentication took place.<p> 944 * <p/> 945 * Note: to use stream compression the smackx.jar file has to be present in the classpath. 946 * 947 * @return true if stream compression negotiation was successful. 948 */ 949 private boolean useCompression() { 950 // If stream compression was offered by the server and we want to use 951 // compression then send compression request to the server 952 if (authenticated) { 953 throw new IllegalStateException("Compression should be negotiated before authentication."); 954 } 955 956 if ((compressionHandler = maybeGetCompressionHandler()) != null) { 957 requestStreamCompression(compressionHandler.getCompressionMethod()); 958 // Wait until compression is being used or a timeout happened 959 synchronized (this) { 960 try { 961 this.wait(SmackConfiguration.getPacketReplyTimeout() * 5); 962 } 963 catch (InterruptedException e) { 964 // Ignore. 965 } 966 } 967 return isUsingCompression(); 968 } 969 return false; 970 } 971 972 /** 973 * Request the server that we want to start using stream compression. When using TLS 974 * then negotiation of stream compression can only happen after TLS was negotiated. If TLS 975 * compression is being used the stream compression should not be used. 976 */ 977 private void requestStreamCompression(String method) { 978 try { 979 writer.write("<compress xmlns='http://jabber.org/protocol/compress'>"); 980 writer.write("<method>" + method + "</method></compress>"); 981 writer.flush(); 982 } 983 catch (IOException e) { 984 notifyConnectionError(e); 985 } 986 } 987 988 /** 989 * Start using stream compression since the server has acknowledged stream compression. 990 * 991 * @throws Exception if there is an exception starting stream compression. 992 */ 993 void startStreamCompression() throws Exception { 994 serverAckdCompression = true; 995 // Initialize the reader and writer with the new secured version 996 initReaderAndWriter(); 997 998 // Set the new writer to use 999 packetWriter.setWriter(writer); 1000 // Send a new opening stream to the server 1001 packetWriter.openStream(); 1002 // Notify that compression is being used 1003 synchronized (this) { 1004 this.notify(); 1005 } 1006 } 1007 1008 /** 1009 * Notifies the XMPP connection that stream compression was denied so that 1010 * the connection process can proceed. 1011 */ 1012 void streamCompressionDenied() { 1013 synchronized (this) { 1014 this.notify(); 1015 } 1016 } 1017 1018 /** 1019 * Establishes a connection to the XMPP server and performs an automatic login 1020 * only if the previous connection state was logged (authenticated). It basically 1021 * creates and maintains a socket connection to the server.<p> 1022 * <p/> 1023 * Listeners will be preserved from a previous connection if the reconnection 1024 * occurs after an abrupt termination. 1025 * 1026 * @throws XMPPException if an error occurs while trying to establish the connection. 1027 * Two possible errors can occur which will be wrapped by an XMPPException -- 1028 * UnknownHostException (XMPP error code 504), and IOException (XMPP error code 1029 * 502). The error codes and wrapped exceptions can be used to present more 1030 * appropriate error messages to end-users. 1031 */ 1032 public void connect() throws XMPPException { 1033 // Establishes the connection, readers and writers 1034 connectUsingConfiguration(config); 1035 // Automatically makes the login if the user was previously connected successfully 1036 // to the server and the connection was terminated abruptly 1037 if (connected && wasAuthenticated) { 1038 // Make the login 1039 if (isAnonymous()) { 1040 // Make the anonymous login 1041 loginAnonymously(); 1042 } 1043 else { 1044 login(config.getUsername(), config.getPassword(), config.getResource()); 1045 } 1046 notifyReconnection(); 1047 } 1048 } 1049 1050 /** 1051 * Sets whether the connection has already logged in the server. 1052 * 1053 * @param wasAuthenticated true if the connection has already been authenticated. 1054 */ 1055 private void setWasAuthenticated(boolean wasAuthenticated) { 1056 if (!this.wasAuthenticated) { 1057 this.wasAuthenticated = wasAuthenticated; 1058 } 1059 } 1060 1061 @Override 1062 public void setRosterStorage(RosterStorage storage) 1063 throws IllegalStateException { 1064 if(roster!=null){ 1065 throw new IllegalStateException("Roster is already initialized"); 1066 } 1067 this.rosterStorage = storage; 1068 } 1069 1070 /** 1071 * Sends out a notification that there was an error with the connection 1072 * and closes the connection. Also prints the stack trace of the given exception 1073 * 1074 * @param e the exception that causes the connection close event. 1075 */ 1076 synchronized void notifyConnectionError(Exception e) { 1077 // Listeners were already notified of the exception, return right here. 1078 if (packetReader.done && packetWriter.done) return; 1079 1080 packetReader.done = true; 1081 packetWriter.done = true; 1082 // Closes the connection temporary. A reconnection is possible 1083 shutdown(new Presence(Presence.Type.unavailable)); 1084 // Print the stack trace to help catch the problem 1085 e.printStackTrace(); 1086 // Notify connection listeners of the error. 1087 for (ConnectionListener listener : getConnectionListeners()) { 1088 try { 1089 listener.connectionClosedOnError(e); 1090 } 1091 catch (Exception e2) { 1092 // Catch and print any exception so we can recover 1093 // from a faulty listener 1094 e2.printStackTrace(); 1095 } 1096 } 1097 } 1098 1099 1100 /** 1101 * Sends a notification indicating that the connection was reconnected successfully. 1102 */ 1103 protected void notifyReconnection() { 1104 // Notify connection listeners of the reconnection. 1105 for (ConnectionListener listener : getConnectionListeners()) { 1106 try { 1107 listener.reconnectionSuccessful(); 1108 } 1109 catch (Exception e) { 1110 // Catch and print any exception so we can recover 1111 // from a faulty listener 1112 e.printStackTrace(); 1113 } 1114 } 1115 } 1116 } 1117