001/** 002 * 003 * Copyright 2003-2007 Jive Software. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.tcp; 018 019import org.jivesoftware.smack.AbstractConnectionListener; 020import org.jivesoftware.smack.AbstractXMPPConnection; 021import org.jivesoftware.smack.ConnectionConfiguration; 022import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; 023import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; 024import org.jivesoftware.smack.StanzaListener; 025import org.jivesoftware.smack.SmackConfiguration; 026import org.jivesoftware.smack.SmackException; 027import org.jivesoftware.smack.SmackException.AlreadyConnectedException; 028import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; 029import org.jivesoftware.smack.SmackException.NoResponseException; 030import org.jivesoftware.smack.SmackException.NotConnectedException; 031import org.jivesoftware.smack.SmackException.ConnectionException; 032import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; 033import org.jivesoftware.smack.SynchronizationPoint; 034import org.jivesoftware.smack.XMPPException.FailedNonzaException; 035import org.jivesoftware.smack.XMPPException.StreamErrorException; 036import org.jivesoftware.smack.XMPPConnection; 037import org.jivesoftware.smack.XMPPException; 038import org.jivesoftware.smack.compress.packet.Compressed; 039import org.jivesoftware.smack.compression.XMPPInputOutputStream; 040import org.jivesoftware.smack.filter.StanzaFilter; 041import org.jivesoftware.smack.compress.packet.Compress; 042import org.jivesoftware.smack.packet.Element; 043import org.jivesoftware.smack.packet.IQ; 044import org.jivesoftware.smack.packet.Message; 045import org.jivesoftware.smack.packet.StreamOpen; 046import org.jivesoftware.smack.packet.Stanza; 047import org.jivesoftware.smack.packet.Presence; 048import org.jivesoftware.smack.packet.StartTls; 049import org.jivesoftware.smack.packet.StreamError; 050import org.jivesoftware.smack.sasl.packet.SaslStreamElements; 051import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Challenge; 052import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; 053import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; 054import org.jivesoftware.smack.sm.SMUtils; 055import org.jivesoftware.smack.sm.StreamManagementException; 056import org.jivesoftware.smack.sm.StreamManagementException.StreamIdDoesNotMatchException; 057import org.jivesoftware.smack.sm.StreamManagementException.StreamManagementCounterError; 058import org.jivesoftware.smack.sm.StreamManagementException.StreamManagementNotEnabledException; 059import org.jivesoftware.smack.sm.packet.StreamManagement; 060import org.jivesoftware.smack.sm.packet.StreamManagement.AckAnswer; 061import org.jivesoftware.smack.sm.packet.StreamManagement.AckRequest; 062import org.jivesoftware.smack.sm.packet.StreamManagement.Enable; 063import org.jivesoftware.smack.sm.packet.StreamManagement.Enabled; 064import org.jivesoftware.smack.sm.packet.StreamManagement.Failed; 065import org.jivesoftware.smack.sm.packet.StreamManagement.Resume; 066import org.jivesoftware.smack.sm.packet.StreamManagement.Resumed; 067import org.jivesoftware.smack.sm.packet.StreamManagement.StreamManagementFeature; 068import org.jivesoftware.smack.sm.predicates.Predicate; 069import org.jivesoftware.smack.sm.provider.ParseStreamManagement; 070import org.jivesoftware.smack.packet.Nonza; 071import org.jivesoftware.smack.proxy.ProxyInfo; 072import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; 073import org.jivesoftware.smack.util.Async; 074import org.jivesoftware.smack.util.DNSUtil; 075import org.jivesoftware.smack.util.PacketParserUtils; 076import org.jivesoftware.smack.util.StringUtils; 077import org.jivesoftware.smack.util.TLSUtils; 078import org.jivesoftware.smack.util.XmlStringBuilder; 079import org.jivesoftware.smack.util.dns.HostAddress; 080import org.jivesoftware.smack.util.dns.SmackDaneProvider; 081import org.jivesoftware.smack.util.dns.SmackDaneVerifier; 082import org.jxmpp.jid.impl.JidCreate; 083import org.jxmpp.jid.parts.Resourcepart; 084import org.jxmpp.stringprep.XmppStringprepException; 085import org.jxmpp.util.XmppStringUtils; 086import org.xmlpull.v1.XmlPullParser; 087import org.xmlpull.v1.XmlPullParserException; 088 089import javax.net.SocketFactory; 090import javax.net.ssl.HostnameVerifier; 091import javax.net.ssl.KeyManager; 092import javax.net.ssl.KeyManagerFactory; 093import javax.net.ssl.SSLContext; 094import javax.net.ssl.SSLSession; 095import javax.net.ssl.SSLSocket; 096import javax.net.ssl.TrustManager; 097import javax.net.ssl.X509TrustManager; 098import javax.security.auth.callback.Callback; 099import javax.security.auth.callback.CallbackHandler; 100import javax.security.auth.callback.PasswordCallback; 101 102import java.io.BufferedReader; 103import java.io.ByteArrayInputStream; 104import java.io.FileInputStream; 105import java.io.IOException; 106import java.io.InputStream; 107import java.io.InputStreamReader; 108import java.io.OutputStream; 109import java.io.OutputStreamWriter; 110import java.io.Writer; 111import java.lang.reflect.Constructor; 112import java.net.InetAddress; 113import java.net.InetSocketAddress; 114import java.net.Socket; 115import java.security.KeyManagementException; 116import java.security.KeyStore; 117import java.security.KeyStoreException; 118import java.security.NoSuchAlgorithmException; 119import java.security.NoSuchProviderException; 120import java.security.Provider; 121import java.security.SecureRandom; 122import java.security.Security; 123import java.security.UnrecoverableKeyException; 124import java.security.cert.CertificateException; 125import java.util.ArrayList; 126import java.util.Collection; 127import java.util.Iterator; 128import java.util.LinkedHashSet; 129import java.util.LinkedList; 130import java.util.List; 131import java.util.Map; 132import java.util.Set; 133import java.util.concurrent.ArrayBlockingQueue; 134import java.util.concurrent.BlockingQueue; 135import java.util.concurrent.ConcurrentHashMap; 136import java.util.concurrent.ConcurrentLinkedQueue; 137import java.util.concurrent.TimeUnit; 138import java.util.concurrent.atomic.AtomicBoolean; 139import java.util.logging.Level; 140import java.util.logging.Logger; 141 142/** 143 * Creates a socket connection to an XMPP server. This is the default connection 144 * to an XMPP server and is specified in the XMPP Core (RFC 6120). 145 * 146 * @see XMPPConnection 147 * @author Matt Tucker 148 */ 149public class XMPPTCPConnection extends AbstractXMPPConnection { 150 151 private static final int QUEUE_SIZE = 500; 152 private static final Logger LOGGER = Logger.getLogger(XMPPTCPConnection.class.getName()); 153 154 /** 155 * The socket which is used for this connection. 156 */ 157 private Socket socket; 158 159 /** 160 * 161 */ 162 private boolean disconnectedButResumeable = false; 163 164 private SSLSocket secureSocket; 165 166 /** 167 * Protected access level because of unit test purposes 168 */ 169 protected PacketWriter packetWriter; 170 171 /** 172 * Protected access level because of unit test purposes 173 */ 174 protected PacketReader packetReader; 175 176 private final SynchronizationPoint<Exception> initalOpenStreamSend = new SynchronizationPoint<>( 177 this, "initial open stream element send to server"); 178 179 /** 180 * 181 */ 182 private final SynchronizationPoint<XMPPException> maybeCompressFeaturesReceived = new SynchronizationPoint<XMPPException>( 183 this, "stream compression feature"); 184 185 /** 186 * 187 */ 188 private final SynchronizationPoint<SmackException> compressSyncPoint = new SynchronizationPoint<>( 189 this, "stream compression"); 190 191 /** 192 * A synchronization point which is successful if this connection has received the closing 193 * stream element from the remote end-point, i.e. the server. 194 */ 195 private final SynchronizationPoint<Exception> closingStreamReceived = new SynchronizationPoint<>( 196 this, "stream closing element received"); 197 198 /** 199 * The default bundle and defer callback, used for new connections. 200 * @see bundleAndDeferCallback 201 */ 202 private static BundleAndDeferCallback defaultBundleAndDeferCallback; 203 204 /** 205 * The used bundle and defer callback. 206 * <p> 207 * Although this field may be set concurrently, the 'volatile' keyword was deliberately not added, in order to avoid 208 * having a 'volatile' read within the writer threads loop. 209 * </p> 210 */ 211 private BundleAndDeferCallback bundleAndDeferCallback = defaultBundleAndDeferCallback; 212 213 private static boolean useSmDefault = true; 214 215 private static boolean useSmResumptionDefault = true; 216 217 /** 218 * The stream ID of the stream that is currently resumable, ie. the stream we hold the state 219 * for in {@link #clientHandledStanzasCount}, {@link #serverHandledStanzasCount} and 220 * {@link #unacknowledgedStanzas}. 221 */ 222 private String smSessionId; 223 224 private final SynchronizationPoint<FailedNonzaException> smResumedSyncPoint = new SynchronizationPoint<>( 225 this, "stream resumed element"); 226 227 private final SynchronizationPoint<SmackException> smEnabledSyncPoint = new SynchronizationPoint<>( 228 this, "stream enabled element"); 229 230 /** 231 * The client's preferred maximum resumption time in seconds. 232 */ 233 private int smClientMaxResumptionTime = -1; 234 235 /** 236 * The server's preferred maximum resumption time in seconds. 237 */ 238 private int smServerMaxResumptimTime = -1; 239 240 /** 241 * Indicates whether Stream Management (XEP-198) should be used if it's supported by the server. 242 */ 243 private boolean useSm = useSmDefault; 244 private boolean useSmResumption = useSmResumptionDefault; 245 246 /** 247 * The counter that the server sends the client about it's current height. For example, if the server sends 248 * {@code <a h='42'/>}, then this will be set to 42 (while also handling the {@link #unacknowledgedStanzas} queue). 249 */ 250 private long serverHandledStanzasCount = 0; 251 252 /** 253 * The counter for stanzas handled ("received") by the client. 254 * <p> 255 * Note that we don't need to synchronize this counter. Although JLS 17.7 states that reads and writes to longs are 256 * not atomic, it guarantees that there are at most 2 separate writes, one to each 32-bit half. And since 257 * {@link SMUtils#incrementHeight(long)} masks the lower 32 bit, we only operate on one half of the long and 258 * therefore have no concurrency problem because the read/write operations on one half are guaranteed to be atomic. 259 * </p> 260 */ 261 private long clientHandledStanzasCount = 0; 262 263 private BlockingQueue<Stanza> unacknowledgedStanzas; 264 265 /** 266 * Set to true if Stream Management was at least once enabled for this connection. 267 */ 268 private boolean smWasEnabledAtLeastOnce = false; 269 270 /** 271 * This listeners are invoked for every stanza that got acknowledged. 272 * <p> 273 * We use a {@link ConccurrentLinkedQueue} here in order to allow the listeners to remove 274 * themselves after they have been invoked. 275 * </p> 276 */ 277 private final Collection<StanzaListener> stanzaAcknowledgedListeners = new ConcurrentLinkedQueue<StanzaListener>(); 278 279 /** 280 * This listeners are invoked for a acknowledged stanza that has the given stanza ID. They will 281 * only be invoked once and automatically removed after that. 282 */ 283 private final Map<String, StanzaListener> stanzaIdAcknowledgedListeners = new ConcurrentHashMap<String, StanzaListener>(); 284 285 /** 286 * Predicates that determine if an stream management ack should be requested from the server. 287 * <p> 288 * We use a linked hash set here, so that the order how the predicates are added matches the 289 * order in which they are invoked in order to determine if an ack request should be send or not. 290 * </p> 291 */ 292 private final Set<StanzaFilter> requestAckPredicates = new LinkedHashSet<StanzaFilter>(); 293 294 private final XMPPTCPConnectionConfiguration config; 295 296 /** 297 * Creates a new XMPP connection over TCP (optionally using proxies). 298 * <p> 299 * Note that XMPPTCPConnection constructors do not establish a connection to the server 300 * and you must call {@link #connect()}. 301 * </p> 302 * 303 * @param config the connection configuration. 304 */ 305 public XMPPTCPConnection(XMPPTCPConnectionConfiguration config) { 306 super(config); 307 this.config = config; 308 addConnectionListener(new AbstractConnectionListener() { 309 @Override 310 public void connectionClosedOnError(Exception e) { 311 if (e instanceof XMPPException.StreamErrorException) { 312 dropSmState(); 313 } 314 } 315 }); 316 } 317 318 /** 319 * Creates a new XMPP connection over TCP. 320 * <p> 321 * Note that {@code jid} must be the bare JID, e.g. "user@example.org". More fine-grained control over the 322 * connection settings is available using the {@link #XMPPTCPConnection(XMPPTCPConnectionConfiguration)} 323 * constructor. 324 * </p> 325 * 326 * @param jid the bare JID used by the client. 327 * @param password the password or authentication token. 328 * @throws XmppStringprepException 329 */ 330 public XMPPTCPConnection(CharSequence jid, String password) throws XmppStringprepException { 331 this(XmppStringUtils.parseLocalpart(jid.toString()), password, XmppStringUtils.parseDomain(jid.toString())); 332 } 333 334 /** 335 * Creates a new XMPP connection over TCP. 336 * <p> 337 * This is the simplest constructor for connecting to an XMPP server. Alternatively, 338 * you can get fine-grained control over connection settings using the 339 * {@link #XMPPTCPConnection(XMPPTCPConnectionConfiguration)} constructor. 340 * </p> 341 * @param username 342 * @param password 343 * @param serviceName 344 * @throws XmppStringprepException 345 */ 346 public XMPPTCPConnection(CharSequence username, String password, String serviceName) throws XmppStringprepException { 347 this(XMPPTCPConnectionConfiguration.builder().setUsernameAndPassword(username, password).setXmppDomain( 348 JidCreate.domainBareFrom(serviceName)).build()); 349 } 350 351 @Override 352 protected void throwNotConnectedExceptionIfAppropriate() throws NotConnectedException { 353 if (packetWriter == null) { 354 throw new NotConnectedException(); 355 } 356 packetWriter.throwNotConnectedExceptionIfDoneAndResumptionNotPossible(); 357 } 358 359 @Override 360 protected void throwAlreadyConnectedExceptionIfAppropriate() throws AlreadyConnectedException { 361 if (isConnected() && !disconnectedButResumeable) { 362 throw new AlreadyConnectedException(); 363 } 364 } 365 366 @Override 367 protected void throwAlreadyLoggedInExceptionIfAppropriate() throws AlreadyLoggedInException { 368 if (isAuthenticated() && !disconnectedButResumeable) { 369 throw new AlreadyLoggedInException(); 370 } 371 } 372 373 @Override 374 protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedException, InterruptedException { 375 // Reset the flag in case it was set 376 disconnectedButResumeable = false; 377 super.afterSuccessfulLogin(resumed); 378 } 379 380 @Override 381 protected synchronized void loginInternal(String username, String password, Resourcepart resource) throws XMPPException, 382 SmackException, IOException, InterruptedException { 383 // Authenticate using SASL 384 SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null; 385 saslAuthentication.authenticate(username, password, config.getAuthzid(), sslSession); 386 387 // If compression is enabled then request the server to use stream compression. XEP-170 388 // recommends to perform stream compression before resource binding. 389 maybeEnableCompression(); 390 391 if (isSmResumptionPossible()) { 392 smResumedSyncPoint.sendAndWaitForResponse(new Resume(clientHandledStanzasCount, smSessionId)); 393 if (smResumedSyncPoint.wasSuccessful()) { 394 // We successfully resumed the stream, be done here 395 afterSuccessfulLogin(true); 396 return; 397 } 398 // SM resumption failed, what Smack does here is to report success of 399 // lastFeaturesReceived in case of sm resumption was answered with 'failed' so that 400 // normal resource binding can be tried. 401 LOGGER.fine("Stream resumption failed, continuing with normal stream establishment process"); 402 } 403 404 List<Stanza> previouslyUnackedStanzas = new LinkedList<Stanza>(); 405 if (unacknowledgedStanzas != null) { 406 // There was a previous connection with SM enabled but that was either not resumable or 407 // failed to resume. Make sure that we (re-)send the unacknowledged stanzas. 408 unacknowledgedStanzas.drainTo(previouslyUnackedStanzas); 409 // Reset unacknowledged stanzas to 'null' to signal that we never send 'enable' in this 410 // XMPP session (There maybe was an enabled in a previous XMPP session of this 411 // connection instance though). This is used in writePackets to decide if stanzas should 412 // be added to the unacknowledged stanzas queue, because they have to be added right 413 // after the 'enable' stream element has been sent. 414 dropSmState(); 415 } 416 417 // Now bind the resource. It is important to do this *after* we dropped an eventually 418 // existing Stream Management state. As otherwise <bind/> and <session/> may end up in 419 // unacknowledgedStanzas and become duplicated on reconnect. See SMACK-706. 420 bindResourceAndEstablishSession(resource); 421 422 if (isSmAvailable() && useSm) { 423 // Remove what is maybe left from previously stream managed sessions 424 serverHandledStanzasCount = 0; 425 // XEP-198 3. Enabling Stream Management. If the server response to 'Enable' is 'Failed' 426 // then this is a non recoverable error and we therefore throw an exception. 427 smEnabledSyncPoint.sendAndWaitForResponseOrThrow(new Enable(useSmResumption, smClientMaxResumptionTime)); 428 synchronized (requestAckPredicates) { 429 if (requestAckPredicates.isEmpty()) { 430 // Assure that we have at lest one predicate set up that so that we request acks 431 // for the server and eventually flush some stanzas from the unacknowledged 432 // stanza queue 433 requestAckPredicates.add(Predicate.forMessagesOrAfter5Stanzas()); 434 } 435 } 436 } 437 // (Re-)send the stanzas *after* we tried to enable SM 438 for (Stanza stanza : previouslyUnackedStanzas) { 439 sendStanzaInternal(stanza); 440 } 441 442 afterSuccessfulLogin(false); 443 } 444 445 @Override 446 public boolean isSecureConnection() { 447 return secureSocket != null; 448 } 449 450 /** 451 * Shuts the current connection down. After this method returns, the connection must be ready 452 * for re-use by connect. 453 */ 454 @Override 455 protected void shutdown() { 456 if (isSmEnabled()) { 457 try { 458 // Try to send a last SM Acknowledgement. Most servers won't find this information helpful, as the SM 459 // state is dropped after a clean disconnect anyways. OTOH it doesn't hurt much either. 460 sendSmAcknowledgementInternal(); 461 } catch (InterruptedException | NotConnectedException e) { 462 LOGGER.log(Level.FINE, "Can not send final SM ack as connection is not connected", e); 463 } 464 } 465 shutdown(false); 466 } 467 468 /** 469 * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. 470 */ 471 public synchronized void instantShutdown() { 472 shutdown(true); 473 } 474 475 private void shutdown(boolean instant) { 476 if (disconnectedButResumeable) { 477 return; 478 } 479 480 // First shutdown the writer, this will result in a closing stream element getting send to 481 // the server 482 if (packetWriter != null) { 483 LOGGER.finer("PacketWriter shutdown()"); 484 packetWriter.shutdown(instant); 485 } 486 LOGGER.finer("PacketWriter has been shut down"); 487 488 if (!instant) { 489 try { 490 // After we send the closing stream element, check if there was already a 491 // closing stream element sent by the server or wait with a timeout for a 492 // closing stream element to be received from the server. 493 @SuppressWarnings("unused") 494 Exception res = closingStreamReceived.checkIfSuccessOrWait(); 495 } catch (InterruptedException | NoResponseException e) { 496 LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, e); 497 } 498 } 499 500 if (packetReader != null) { 501 LOGGER.finer("PacketReader shutdown()"); 502 packetReader.shutdown(); 503 } 504 LOGGER.finer("PacketReader has been shut down"); 505 506 try { 507 socket.close(); 508 } catch (Exception e) { 509 LOGGER.log(Level.WARNING, "shutdown", e); 510 } 511 512 setWasAuthenticated(); 513 // If we are able to resume the stream, then don't set 514 // connected/authenticated/usingTLS to false since we like behave like we are still 515 // connected (e.g. sendStanza should not throw a NotConnectedException). 516 if (isSmResumptionPossible() && instant) { 517 disconnectedButResumeable = true; 518 } else { 519 disconnectedButResumeable = false; 520 // Reset the stream management session id to null, since if the stream is cleanly closed, i.e. sending a closing 521 // stream tag, there is no longer a stream to resume. 522 smSessionId = null; 523 } 524 authenticated = false; 525 connected = false; 526 secureSocket = null; 527 reader = null; 528 writer = null; 529 530 maybeCompressFeaturesReceived.init(); 531 compressSyncPoint.init(); 532 smResumedSyncPoint.init(); 533 smEnabledSyncPoint.init(); 534 initalOpenStreamSend.init(); 535 } 536 537 @Override 538 public void sendNonza(Nonza element) throws NotConnectedException, InterruptedException { 539 packetWriter.sendStreamElement(element); 540 } 541 542 @Override 543 protected void sendStanzaInternal(Stanza packet) throws NotConnectedException, InterruptedException { 544 packetWriter.sendStreamElement(packet); 545 if (isSmEnabled()) { 546 for (StanzaFilter requestAckPredicate : requestAckPredicates) { 547 if (requestAckPredicate.accept(packet)) { 548 requestSmAcknowledgementInternal(); 549 break; 550 } 551 } 552 } 553 } 554 555 private void connectUsingConfiguration() throws ConnectionException, IOException { 556 List<HostAddress> failedAddresses = populateHostAddresses(); 557 SocketFactory socketFactory = config.getSocketFactory(); 558 ProxyInfo proxyInfo = config.getProxyInfo(); 559 int timeout = config.getConnectTimeout(); 560 if (socketFactory == null) { 561 socketFactory = SocketFactory.getDefault(); 562 } 563 for (HostAddress hostAddress : hostAddresses) { 564 Iterator<InetAddress> inetAddresses = null; 565 String host = hostAddress.getFQDN(); 566 int port = hostAddress.getPort(); 567 if (proxyInfo == null) { 568 inetAddresses = hostAddress.getInetAddresses().iterator(); 569 assert(inetAddresses.hasNext()); 570 571 innerloop: while (inetAddresses.hasNext()) { 572 // Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not 573 // re-usable after a failed connection attempt. See also SMACK-724. 574 socket = socketFactory.createSocket(); 575 576 final InetAddress inetAddress = inetAddresses.next(); 577 final String inetAddressAndPort = inetAddress + " at port " + port; 578 LOGGER.finer("Trying to establish TCP connection to " + inetAddressAndPort); 579 try { 580 socket.connect(new InetSocketAddress(inetAddress, port), timeout); 581 } catch (Exception e) { 582 hostAddress.setException(inetAddress, e); 583 if (inetAddresses.hasNext()) { 584 continue innerloop; 585 } else { 586 break innerloop; 587 } 588 } 589 LOGGER.finer("Established TCP connection to " + inetAddressAndPort); 590 // We found a host to connect to, return here 591 this.host = host; 592 this.port = port; 593 return; 594 } 595 failedAddresses.add(hostAddress); 596 } else { 597 socket = socketFactory.createSocket(); 598 StringUtils.requireNotNullOrEmpty(host, "Host of HostAddress " + hostAddress + " must not be null when using a Proxy"); 599 final String hostAndPort = host + " at port " + port; 600 LOGGER.finer("Trying to establish TCP connection via Proxy to " + hostAndPort); 601 try { 602 proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout); 603 } catch (IOException e) { 604 hostAddress.setException(e); 605 continue; 606 } 607 LOGGER.finer("Established TCP connection to " + hostAndPort); 608 // We found a host to connect to, return here 609 this.host = host; 610 this.port = port; 611 return; 612 } 613 } 614 // There are no more host addresses to try 615 // throw an exception and report all tried 616 // HostAddresses in the exception 617 throw ConnectionException.from(failedAddresses); 618 } 619 620 /** 621 * Initializes the connection by creating a stanza(/packet) reader and writer and opening a 622 * XMPP stream to the server. 623 * 624 * @throws XMPPException if establishing a connection to the server fails. 625 * @throws SmackException if the server failes to respond back or if there is anther error. 626 * @throws IOException 627 */ 628 private void initConnection() throws IOException { 629 boolean isFirstInitialization = packetReader == null || packetWriter == null; 630 compressionHandler = null; 631 632 // Set the reader and writer instance variables 633 initReaderAndWriter(); 634 635 if (isFirstInitialization) { 636 packetWriter = new PacketWriter(); 637 packetReader = new PacketReader(); 638 639 // If debugging is enabled, we should start the thread that will listen for 640 // all packets and then log them. 641 if (config.isDebuggerEnabled()) { 642 addAsyncStanzaListener(debugger.getReaderListener(), null); 643 if (debugger.getWriterListener() != null) { 644 addPacketSendingListener(debugger.getWriterListener(), null); 645 } 646 } 647 } 648 // Start the packet writer. This will open an XMPP stream to the server 649 packetWriter.init(); 650 // Start the packet reader. The startup() method will block until we 651 // get an opening stream packet back from server 652 packetReader.init(); 653 } 654 655 private void initReaderAndWriter() throws IOException { 656 InputStream is = socket.getInputStream(); 657 OutputStream os = socket.getOutputStream(); 658 if (compressionHandler != null) { 659 is = compressionHandler.getInputStream(is); 660 os = compressionHandler.getOutputStream(os); 661 } 662 // OutputStreamWriter is already buffered, no need to wrap it into a BufferedWriter 663 writer = new OutputStreamWriter(os, "UTF-8"); 664 reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); 665 666 // If debugging is enabled, we open a window and write out all network traffic. 667 initDebugger(); 668 } 669 670 /** 671 * The server has indicated that TLS negotiation can start. We now need to secure the 672 * existing plain connection and perform a handshake. This method won't return until the 673 * connection has finished the handshake or an error occurred while securing the connection. 674 * @throws IOException 675 * @throws CertificateException 676 * @throws NoSuchAlgorithmException 677 * @throws NoSuchProviderException 678 * @throws KeyStoreException 679 * @throws UnrecoverableKeyException 680 * @throws KeyManagementException 681 * @throws SmackException 682 * @throws Exception if an exception occurs. 683 */ 684 private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SmackException { 685 SSLContext context = this.config.getCustomSSLContext(); 686 KeyStore ks = null; 687 KeyManager[] kms = null; 688 PasswordCallback pcb = null; 689 SmackDaneVerifier daneVerifier = null; 690 691 if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) { 692 SmackDaneProvider daneProvider = DNSUtil.getDaneProvider(); 693 if (daneProvider == null) { 694 throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured"); 695 } 696 daneVerifier = daneProvider.newInstance(); 697 if (daneVerifier == null) { 698 throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier"); 699 } 700 } 701 702 if (context == null) { 703 final String keyStoreType = config.getKeystoreType(); 704 final CallbackHandler callbackHandler = config.getCallbackHandler(); 705 final String keystorePath = config.getKeystorePath(); 706 if ("PKCS11".equals(keyStoreType)) { 707 try { 708 Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class); 709 String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library(); 710 ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes(StringUtils.UTF8)); 711 Provider p = (Provider)c.newInstance(config); 712 Security.addProvider(p); 713 ks = KeyStore.getInstance("PKCS11",p); 714 pcb = new PasswordCallback("PKCS11 Password: ",false); 715 callbackHandler.handle(new Callback[]{pcb}); 716 ks.load(null,pcb.getPassword()); 717 } 718 catch (Exception e) { 719 LOGGER.log(Level.WARNING, "Exception", e); 720 ks = null; 721 } 722 } 723 else if ("Apple".equals(keyStoreType)) { 724 ks = KeyStore.getInstance("KeychainStore","Apple"); 725 ks.load(null,null); 726 //pcb = new PasswordCallback("Apple Keychain",false); 727 //pcb.setPassword(null); 728 } 729 else if (keyStoreType != null){ 730 ks = KeyStore.getInstance(keyStoreType); 731 if (callbackHandler != null && StringUtils.isNotEmpty(keystorePath)) { 732 try { 733 pcb = new PasswordCallback("Keystore Password: ", false); 734 callbackHandler.handle(new Callback[] { pcb }); 735 ks.load(new FileInputStream(keystorePath), pcb.getPassword()); 736 } 737 catch (Exception e) { 738 LOGGER.log(Level.WARNING, "Exception", e); 739 ks = null; 740 } 741 } else { 742 ks.load(null, null); 743 } 744 } 745 746 if (ks != null) { 747 KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 748 try { 749 if (pcb == null) { 750 kmf.init(ks, null); 751 } 752 else { 753 kmf.init(ks, pcb.getPassword()); 754 pcb.clearPassword(); 755 } 756 kms = kmf.getKeyManagers(); 757 } 758 catch (NullPointerException npe) { 759 LOGGER.log(Level.WARNING, "NullPointerException", npe); 760 } 761 } 762 763 // If the user didn't specify a SSLContext, use the default one 764 context = SSLContext.getInstance("TLS"); 765 766 final SecureRandom secureRandom = new java.security.SecureRandom(); 767 X509TrustManager customTrustManager = config.getCustomX509TrustManager(); 768 769 if (daneVerifier != null) { 770 // User requested DANE verification. 771 daneVerifier.init(context, kms, customTrustManager, secureRandom); 772 } else { 773 TrustManager[] customTrustManagers = null; 774 if (customTrustManager != null) { 775 customTrustManagers = new TrustManager[] { customTrustManager }; 776 } 777 context.init(kms, customTrustManagers, secureRandom); 778 } 779 } 780 781 Socket plain = socket; 782 // Secure the plain connection 783 socket = context.getSocketFactory().createSocket(plain, 784 host, plain.getPort(), true); 785 786 final SSLSocket sslSocket = (SSLSocket) socket; 787 // Immediately set the enabled SSL protocols and ciphers. See SMACK-712 why this is 788 // important (at least on certain platforms) and it seems to be a good idea anyways to 789 // prevent an accidental implicit handshake. 790 TLSUtils.setEnabledProtocolsAndCiphers(sslSocket, config.getEnabledSSLProtocols(), config.getEnabledSSLCiphers()); 791 792 // Initialize the reader and writer with the new secured version 793 initReaderAndWriter(); 794 795 // Proceed to do the handshake 796 sslSocket.startHandshake(); 797 798 if (daneVerifier != null) { 799 daneVerifier.finish(sslSocket); 800 } 801 802 final HostnameVerifier verifier = getConfiguration().getHostnameVerifier(); 803 if (verifier == null) { 804 throw new IllegalStateException("No HostnameVerifier set. Use connectionConfiguration.setHostnameVerifier() to configure."); 805 } else if (!verifier.verify(getXMPPServiceDomain().toString(), sslSocket.getSession())) { 806 throw new CertificateException("Hostname verification of certificate failed. Certificate does not authenticate " + getXMPPServiceDomain()); 807 } 808 809 // Set that TLS was successful 810 secureSocket = sslSocket; 811 } 812 813 /** 814 * Returns the compression handler that can be used for one compression methods offered by the server. 815 * 816 * @return a instance of XMPPInputOutputStream or null if no suitable instance was found 817 * 818 */ 819 private static XMPPInputOutputStream maybeGetCompressionHandler(Compress.Feature compression) { 820 for (XMPPInputOutputStream handler : SmackConfiguration.getCompresionHandlers()) { 821 String method = handler.getCompressionMethod(); 822 if (compression.getMethods().contains(method)) 823 return handler; 824 } 825 return null; 826 } 827 828 @Override 829 public boolean isUsingCompression() { 830 return compressionHandler != null && compressSyncPoint.wasSuccessful(); 831 } 832 833 /** 834 * <p> 835 * Starts using stream compression that will compress network traffic. Traffic can be 836 * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network 837 * connection. However, the server and the client will need to use more CPU time in order to 838 * un/compress network data so under high load the server performance might be affected. 839 * </p> 840 * <p> 841 * Stream compression has to have been previously offered by the server. Currently only the 842 * zlib method is supported by the client. Stream compression negotiation has to be done 843 * before authentication took place. 844 * </p> 845 * 846 * @throws NotConnectedException 847 * @throws SmackException 848 * @throws NoResponseException 849 * @throws InterruptedException 850 */ 851 private void maybeEnableCompression() throws NotConnectedException, NoResponseException, SmackException, InterruptedException { 852 if (!config.isCompressionEnabled()) { 853 return; 854 } 855 maybeCompressFeaturesReceived.checkIfSuccessOrWait(); 856 Compress.Feature compression = getFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE); 857 if (compression == null) { 858 // Server does not support compression 859 return; 860 } 861 // If stream compression was offered by the server and we want to use 862 // compression then send compression request to the server 863 if ((compressionHandler = maybeGetCompressionHandler(compression)) != null) { 864 compressSyncPoint.sendAndWaitForResponseOrThrow(new Compress(compressionHandler.getCompressionMethod())); 865 } else { 866 LOGGER.warning("Could not enable compression because no matching handler/method pair was found"); 867 } 868 } 869 870 /** 871 * Establishes a connection to the XMPP server. It basically 872 * creates and maintains a socket connection to the server. 873 * <p> 874 * Listeners will be preserved from a previous connection if the reconnection 875 * occurs after an abrupt termination. 876 * </p> 877 * 878 * @throws XMPPException if an error occurs while trying to establish the connection. 879 * @throws SmackException 880 * @throws IOException 881 * @throws InterruptedException 882 */ 883 @Override 884 protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { 885 closingStreamReceived.init(); 886 // Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if 887 // there is an error establishing the connection 888 connectUsingConfiguration(); 889 890 // We connected successfully to the servers TCP port 891 initConnection(); 892 } 893 894 /** 895 * Sends out a notification that there was an error with the connection 896 * and closes the connection. Also prints the stack trace of the given exception 897 * 898 * @param e the exception that causes the connection close event. 899 */ 900 private synchronized void notifyConnectionError(Exception e) { 901 // Listeners were already notified of the exception, return right here. 902 if ((packetReader == null || packetReader.done) && 903 (packetWriter == null || packetWriter.done())) return; 904 905 // Closes the connection temporary. A reconnection is possible 906 // Note that a connection listener of XMPPTCPConnection will drop the SM state in 907 // case the Exception is a StreamErrorException. 908 instantShutdown(); 909 910 // Notify connection listeners of the error. 911 callConnectionClosedOnErrorListener(e); 912 } 913 914 /** 915 * For unit testing purposes 916 * 917 * @param writer 918 */ 919 protected void setWriter(Writer writer) { 920 this.writer = writer; 921 } 922 923 @Override 924 protected void afterFeaturesReceived() throws NotConnectedException, InterruptedException { 925 StartTls startTlsFeature = getFeature(StartTls.ELEMENT, StartTls.NAMESPACE); 926 if (startTlsFeature != null) { 927 if (startTlsFeature.required() && config.getSecurityMode() == SecurityMode.disabled) { 928 SmackException smackException = new SecurityRequiredByServerException(); 929 tlsHandled.reportFailure(smackException); 930 notifyConnectionError(smackException); 931 return; 932 } 933 934 if (config.getSecurityMode() != ConnectionConfiguration.SecurityMode.disabled) { 935 sendNonza(new StartTls()); 936 } else { 937 tlsHandled.reportSuccess(); 938 } 939 } else { 940 tlsHandled.reportSuccess(); 941 } 942 943 if (getSASLAuthentication().authenticationSuccessful()) { 944 // If we have received features after the SASL has been successfully completed, then we 945 // have also *maybe* received, as it is an optional feature, the compression feature 946 // from the server. 947 maybeCompressFeaturesReceived.reportSuccess(); 948 } 949 } 950 951 /** 952 * Resets the parser using the latest connection's reader. Reseting the parser is necessary 953 * when the plain connection has been secured or when a new opening stream element is going 954 * to be sent by the server. 955 * 956 * @throws SmackException if the parser could not be reset. 957 * @throws InterruptedException 958 */ 959 void openStream() throws SmackException, InterruptedException { 960 // If possible, provide the receiving entity of the stream open tag, i.e. the server, as much information as 961 // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external 962 // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first 963 // response from the server (see e.g. RFC 6120 ยง 9.1.1 Step 2.) 964 CharSequence to = getXMPPServiceDomain(); 965 CharSequence from = null; 966 CharSequence localpart = config.getUsername(); 967 if (localpart != null) { 968 from = XmppStringUtils.completeJidFrom(localpart, to); 969 } 970 String id = getStreamId(); 971 sendNonza(new StreamOpen(to, from, id)); 972 try { 973 packetReader.parser = PacketParserUtils.newXmppParser(reader); 974 } 975 catch (XmlPullParserException e) { 976 throw new SmackException(e); 977 } 978 } 979 980 protected class PacketReader { 981 982 XmlPullParser parser; 983 984 private volatile boolean done; 985 986 /** 987 * Initializes the reader in order to be used. The reader is initialized during the 988 * first connection and when reconnecting due to an abruptly disconnection. 989 */ 990 void init() { 991 done = false; 992 993 Async.go(new Runnable() { 994 @Override 995 public void run() { 996 parsePackets(); 997 } 998 }, "Smack Packet Reader (" + getConnectionCounter() + ")"); 999 } 1000 1001 /** 1002 * Shuts the stanza(/packet) reader down. This method simply sets the 'done' flag to true. 1003 */ 1004 void shutdown() { 1005 done = true; 1006 } 1007 1008 /** 1009 * Parse top-level packets in order to process them further. 1010 * 1011 * @param thread the thread that is being used by the reader to parse incoming packets. 1012 */ 1013 private void parsePackets() { 1014 try { 1015 initalOpenStreamSend.checkIfSuccessOrWait(); 1016 int eventType = parser.getEventType(); 1017 while (!done) { 1018 switch (eventType) { 1019 case XmlPullParser.START_TAG: 1020 final String name = parser.getName(); 1021 switch (name) { 1022 case Message.ELEMENT: 1023 case IQ.IQ_ELEMENT: 1024 case Presence.ELEMENT: 1025 try { 1026 parseAndProcessStanza(parser); 1027 } finally { 1028 clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); 1029 } 1030 break; 1031 case "stream": 1032 // We found an opening stream. 1033 if ("jabber:client".equals(parser.getNamespace(null))) { 1034 streamId = parser.getAttributeValue("", "id"); 1035 String reportedServerDomain = parser.getAttributeValue("", "from"); 1036 assert(config.getXMPPServiceDomain().equals(reportedServerDomain)); 1037 } 1038 break; 1039 case "error": 1040 StreamError streamError = PacketParserUtils.parseStreamError(parser); 1041 saslFeatureReceived.reportFailure(new StreamErrorException(streamError)); 1042 // Mark the tlsHandled sync point as success, we will use the saslFeatureReceived sync 1043 // point to report the error, which is checked immediately after tlsHandled in 1044 // connectInternal(). 1045 tlsHandled.reportSuccess(); 1046 throw new StreamErrorException(streamError); 1047 case "features": 1048 parseFeatures(parser); 1049 break; 1050 case "proceed": 1051 try { 1052 // Secure the connection by negotiating TLS 1053 proceedTLSReceived(); 1054 // Send a new opening stream to the server 1055 openStream(); 1056 } 1057 catch (Exception e) { 1058 SmackException smackException = new SmackException(e); 1059 tlsHandled.reportFailure(smackException); 1060 throw e; 1061 } 1062 break; 1063 case "failure": 1064 String namespace = parser.getNamespace(null); 1065 switch (namespace) { 1066 case "urn:ietf:params:xml:ns:xmpp-tls": 1067 // TLS negotiation has failed. The server will close the connection 1068 // TODO Parse failure stanza 1069 throw new SmackException("TLS negotiation has failed"); 1070 case "http://jabber.org/protocol/compress": 1071 // Stream compression has been denied. This is a recoverable 1072 // situation. It is still possible to authenticate and 1073 // use the connection but using an uncompressed connection 1074 // TODO Parse failure stanza 1075 compressSyncPoint.reportFailure(new SmackException( 1076 "Could not establish compression")); 1077 break; 1078 case SaslStreamElements.NAMESPACE: 1079 // SASL authentication has failed. The server may close the connection 1080 // depending on the number of retries 1081 final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser); 1082 getSASLAuthentication().authenticationFailed(failure); 1083 break; 1084 } 1085 break; 1086 case Challenge.ELEMENT: 1087 // The server is challenging the SASL authentication made by the client 1088 String challengeData = parser.nextText(); 1089 getSASLAuthentication().challengeReceived(challengeData); 1090 break; 1091 case Success.ELEMENT: 1092 Success success = new Success(parser.nextText()); 1093 // We now need to bind a resource for the connection 1094 // Open a new stream and wait for the response 1095 openStream(); 1096 // The SASL authentication with the server was successful. The next step 1097 // will be to bind the resource 1098 getSASLAuthentication().authenticated(success); 1099 break; 1100 case Compressed.ELEMENT: 1101 // Server confirmed that it's possible to use stream compression. Start 1102 // stream compression 1103 // Initialize the reader and writer with the new compressed version 1104 initReaderAndWriter(); 1105 // Send a new opening stream to the server 1106 openStream(); 1107 // Notify that compression is being used 1108 compressSyncPoint.reportSuccess(); 1109 break; 1110 case Enabled.ELEMENT: 1111 Enabled enabled = ParseStreamManagement.enabled(parser); 1112 if (enabled.isResumeSet()) { 1113 smSessionId = enabled.getId(); 1114 if (StringUtils.isNullOrEmpty(smSessionId)) { 1115 SmackException xmppException = new SmackException("Stream Management 'enabled' element with resume attribute but without session id received"); 1116 smEnabledSyncPoint.reportFailure(xmppException); 1117 throw xmppException; 1118 } 1119 smServerMaxResumptimTime = enabled.getMaxResumptionTime(); 1120 } else { 1121 // Mark this a non-resumable stream by setting smSessionId to null 1122 smSessionId = null; 1123 } 1124 clientHandledStanzasCount = 0; 1125 smWasEnabledAtLeastOnce = true; 1126 smEnabledSyncPoint.reportSuccess(); 1127 LOGGER.fine("Stream Management (XEP-198): succesfully enabled"); 1128 break; 1129 case Failed.ELEMENT: 1130 Failed failed = ParseStreamManagement.failed(parser); 1131 FailedNonzaException xmppException = new FailedNonzaException(failed, failed.getXMPPErrorCondition()); 1132 // If only XEP-198 would specify different failure elements for the SM 1133 // enable and SM resume failure case. But this is not the case, so we 1134 // need to determine if this is a 'Failed' response for either 'Enable' 1135 // or 'Resume'. 1136 if (smResumedSyncPoint.requestSent()) { 1137 smResumedSyncPoint.reportFailure(xmppException); 1138 } 1139 else { 1140 if (!smEnabledSyncPoint.requestSent()) { 1141 throw new IllegalStateException("Failed element received but SM was not previously enabled"); 1142 } 1143 smEnabledSyncPoint.reportFailure(new SmackException(xmppException)); 1144 // Report success for last lastFeaturesReceived so that in case a 1145 // failed resumption, we can continue with normal resource binding. 1146 // See text of XEP-198 5. below Example 11. 1147 lastFeaturesReceived.reportSuccess(); 1148 } 1149 break; 1150 case Resumed.ELEMENT: 1151 Resumed resumed = ParseStreamManagement.resumed(parser); 1152 if (!smSessionId.equals(resumed.getPrevId())) { 1153 throw new StreamIdDoesNotMatchException(smSessionId, resumed.getPrevId()); 1154 } 1155 // Mark SM as enabled and resumption as successful. 1156 smResumedSyncPoint.reportSuccess(); 1157 smEnabledSyncPoint.reportSuccess(); 1158 // First, drop the stanzas already handled by the server 1159 processHandledCount(resumed.getHandledCount()); 1160 // Then re-send what is left in the unacknowledged queue 1161 List<Stanza> stanzasToResend = new ArrayList<>(unacknowledgedStanzas.size()); 1162 unacknowledgedStanzas.drainTo(stanzasToResend); 1163 for (Stanza stanza : stanzasToResend) { 1164 sendStanzaInternal(stanza); 1165 } 1166 // If there where stanzas resent, then request a SM ack for them. 1167 // Writer's sendStreamElement() won't do it automatically based on 1168 // predicates. 1169 if (!stanzasToResend.isEmpty()) { 1170 requestSmAcknowledgementInternal(); 1171 } 1172 LOGGER.fine("Stream Management (XEP-198): Stream resumed"); 1173 break; 1174 case AckAnswer.ELEMENT: 1175 AckAnswer ackAnswer = ParseStreamManagement.ackAnswer(parser); 1176 processHandledCount(ackAnswer.getHandledCount()); 1177 break; 1178 case AckRequest.ELEMENT: 1179 ParseStreamManagement.ackRequest(parser); 1180 if (smEnabledSyncPoint.wasSuccessful()) { 1181 sendSmAcknowledgementInternal(); 1182 } else { 1183 LOGGER.warning("SM Ack Request received while SM is not enabled"); 1184 } 1185 break; 1186 default: 1187 LOGGER.warning("Unknown top level stream element: " + name); 1188 break; 1189 } 1190 break; 1191 case XmlPullParser.END_TAG: 1192 if (parser.getName().equals("stream")) { 1193 if (!parser.getNamespace().equals("http://etherx.jabber.org/streams")) { 1194 LOGGER.warning(XMPPTCPConnection.this + " </stream> but different namespace " + parser.getNamespace()); 1195 break; 1196 } 1197 1198 // Check if the queue was already shut down before reporting success on closing stream tag 1199 // received. This avoids a race if there is a disconnect(), followed by a connect(), which 1200 // did re-start the queue again, causing this writer to assume that the queue is not 1201 // shutdown, which results in a call to disconnect(). 1202 final boolean queueWasShutdown = packetWriter.queue.isShutdown(); 1203 closingStreamReceived.reportSuccess(); 1204 1205 if (queueWasShutdown) { 1206 // We received a closing stream element *after* we initiated the 1207 // termination of the session by sending a closing stream element to 1208 // the server first 1209 return; 1210 } else { 1211 // We received a closing stream element from the server without us 1212 // sending a closing stream element first. This means that the 1213 // server wants to terminate the session, therefore disconnect 1214 // the connection 1215 LOGGER.info(XMPPTCPConnection.this 1216 + " received closing </stream> element." 1217 + " Server wants to terminate the connection, calling disconnect()"); 1218 disconnect(); 1219 } 1220 } 1221 break; 1222 case XmlPullParser.END_DOCUMENT: 1223 // END_DOCUMENT only happens in an error case, as otherwise we would see a 1224 // closing stream element before. 1225 throw new SmackException( 1226 "Parser got END_DOCUMENT event. This could happen e.g. if the server closed the connection without sending a closing stream element"); 1227 } 1228 eventType = parser.next(); 1229 } 1230 } 1231 catch (Exception e) { 1232 closingStreamReceived.reportFailure(e); 1233 // The exception can be ignored if the the connection is 'done' 1234 // or if the it was caused because the socket got closed 1235 if (!(done || packetWriter.queue.isShutdown())) { 1236 // Close the connection and notify connection listeners of the 1237 // error. 1238 notifyConnectionError(e); 1239 } 1240 } 1241 } 1242 } 1243 1244 protected class PacketWriter { 1245 public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; 1246 1247 private final ArrayBlockingQueueWithShutdown<Element> queue = new ArrayBlockingQueueWithShutdown<Element>( 1248 QUEUE_SIZE, true); 1249 1250 /** 1251 * Needs to be protected for unit testing purposes. 1252 */ 1253 protected SynchronizationPoint<NoResponseException> shutdownDone = new SynchronizationPoint<NoResponseException>( 1254 XMPPTCPConnection.this, "shutdown completed"); 1255 1256 /** 1257 * If set, the stanza(/packet) writer is shut down 1258 */ 1259 protected volatile Long shutdownTimestamp = null; 1260 1261 private volatile boolean instantShutdown; 1262 1263 /** 1264 * True if some preconditions are given to start the bundle and defer mechanism. 1265 * <p> 1266 * This will likely get set to true right after the start of the writer thread, because 1267 * {@link #nextStreamElement()} will check if {@link queue} is empty, which is probably the case, and then set 1268 * this field to true. 1269 * </p> 1270 */ 1271 private boolean shouldBundleAndDefer; 1272 1273 /** 1274 * Initializes the writer in order to be used. It is called at the first connection and also 1275 * is invoked if the connection is disconnected by an error. 1276 */ 1277 void init() { 1278 shutdownDone.init(); 1279 shutdownTimestamp = null; 1280 1281 if (unacknowledgedStanzas != null) { 1282 // It's possible that there are new stanzas in the writer queue that 1283 // came in while we were disconnected but resumable, drain those into 1284 // the unacknowledged queue so that they get resent now 1285 drainWriterQueueToUnacknowledgedStanzas(); 1286 } 1287 1288 queue.start(); 1289 Async.go(new Runnable() { 1290 @Override 1291 public void run() { 1292 writePackets(); 1293 } 1294 }, "Smack Packet Writer (" + getConnectionCounter() + ")"); 1295 } 1296 1297 private boolean done() { 1298 return shutdownTimestamp != null; 1299 } 1300 1301 protected void throwNotConnectedExceptionIfDoneAndResumptionNotPossible() throws NotConnectedException { 1302 final boolean done = done(); 1303 if (done) { 1304 final boolean smResumptionPossbile = isSmResumptionPossible(); 1305 // Don't throw a NotConnectedException is there is an resumable stream available 1306 if (!smResumptionPossbile) { 1307 throw new NotConnectedException(XMPPTCPConnection.this, "done=" + done 1308 + " smResumptionPossible=" + smResumptionPossbile); 1309 } 1310 } 1311 } 1312 1313 /** 1314 * Sends the specified element to the server. 1315 * 1316 * @param element the element to send. 1317 * @throws NotConnectedException 1318 * @throws InterruptedException 1319 */ 1320 protected void sendStreamElement(Element element) throws NotConnectedException, InterruptedException { 1321 throwNotConnectedExceptionIfDoneAndResumptionNotPossible(); 1322 try { 1323 queue.put(element); 1324 } 1325 catch (InterruptedException e) { 1326 // put() may throw an InterruptedException for two reasons: 1327 // 1. If the queue was shut down 1328 // 2. If the thread was interrupted 1329 // so we have to check which is the case 1330 throwNotConnectedExceptionIfDoneAndResumptionNotPossible(); 1331 // If the method above did not throw, then the sending thread was interrupted 1332 throw e; 1333 } 1334 } 1335 1336 /** 1337 * Shuts down the stanza(/packet) writer. Once this method has been called, no further 1338 * packets will be written to the server. 1339 * @throws InterruptedException 1340 */ 1341 void shutdown(boolean instant) { 1342 instantShutdown = instant; 1343 queue.shutdown(); 1344 shutdownTimestamp = System.currentTimeMillis(); 1345 try { 1346 shutdownDone.checkIfSuccessOrWait(); 1347 } 1348 catch (NoResponseException | InterruptedException e) { 1349 LOGGER.log(Level.WARNING, "shutdownDone was not marked as successful by the writer thread", e); 1350 } 1351 } 1352 1353 /** 1354 * Maybe return the next available element from the queue for writing. If the queue is shut down <b>or</b> a 1355 * spurious interrupt occurs, <code>null</code> is returned. So it is important to check the 'done' condition in 1356 * that case. 1357 * 1358 * @return the next element for writing or null. 1359 */ 1360 private Element nextStreamElement() { 1361 // It is important the we check if the queue is empty before removing an element from it 1362 if (queue.isEmpty()) { 1363 shouldBundleAndDefer = true; 1364 } 1365 Element packet = null; 1366 try { 1367 packet = queue.take(); 1368 } 1369 catch (InterruptedException e) { 1370 if (!queue.isShutdown()) { 1371 // Users shouldn't try to interrupt the packet writer thread 1372 LOGGER.log(Level.WARNING, "Packet writer thread was interrupted. Don't do that. Use disconnect() instead.", e); 1373 } 1374 } 1375 return packet; 1376 } 1377 1378 private void writePackets() { 1379 Exception writerException = null; 1380 try { 1381 openStream(); 1382 initalOpenStreamSend.reportSuccess(); 1383 // Write out packets from the queue. 1384 while (!done()) { 1385 Element element = nextStreamElement(); 1386 if (element == null) { 1387 continue; 1388 } 1389 1390 // Get a local version of the bundle and defer callback, in case it's unset 1391 // between the null check and the method invocation 1392 final BundleAndDeferCallback localBundleAndDeferCallback = bundleAndDeferCallback; 1393 // If the preconditions are given (e.g. bundleAndDefer callback is set, queue is 1394 // empty), then we could wait a bit for further stanzas attempting to decrease 1395 // our energy consumption 1396 if (localBundleAndDeferCallback != null && isAuthenticated() && shouldBundleAndDefer) { 1397 // Reset shouldBundleAndDefer to false, nextStreamElement() will set it to true once the 1398 // queue is empty again. 1399 shouldBundleAndDefer = false; 1400 final AtomicBoolean bundlingAndDeferringStopped = new AtomicBoolean(); 1401 final int bundleAndDeferMillis = localBundleAndDeferCallback.getBundleAndDeferMillis(new BundleAndDefer( 1402 bundlingAndDeferringStopped)); 1403 if (bundleAndDeferMillis > 0) { 1404 long remainingWait = bundleAndDeferMillis; 1405 final long waitStart = System.currentTimeMillis(); 1406 synchronized (bundlingAndDeferringStopped) { 1407 while (!bundlingAndDeferringStopped.get() && remainingWait > 0) { 1408 bundlingAndDeferringStopped.wait(remainingWait); 1409 remainingWait = bundleAndDeferMillis 1410 - (System.currentTimeMillis() - waitStart); 1411 } 1412 } 1413 } 1414 } 1415 1416 Stanza packet = null; 1417 if (element instanceof Stanza) { 1418 packet = (Stanza) element; 1419 } 1420 else if (element instanceof Enable) { 1421 // The client needs to add messages to the unacknowledged stanzas queue 1422 // right after it sent 'enabled'. Stanza will be added once 1423 // unacknowledgedStanzas is not null. 1424 unacknowledgedStanzas = new ArrayBlockingQueue<>(QUEUE_SIZE); 1425 } 1426 // Check if the stream element should be put to the unacknowledgedStanza 1427 // queue. Note that we can not do the put() in sendStanzaInternal() and the 1428 // packet order is not stable at this point (sendStanzaInternal() can be 1429 // called concurrently). 1430 if (unacknowledgedStanzas != null && packet != null) { 1431 // If the unacknowledgedStanza queue is nearly full, request an new ack 1432 // from the server in order to drain it 1433 if (unacknowledgedStanzas.size() == 0.8 * XMPPTCPConnection.QUEUE_SIZE) { 1434 writer.write(AckRequest.INSTANCE.toXML().toString()); 1435 writer.flush(); 1436 } 1437 try { 1438 // It is important the we put the stanza in the unacknowledged stanza 1439 // queue before we put it on the wire 1440 unacknowledgedStanzas.put(packet); 1441 } 1442 catch (InterruptedException e) { 1443 throw new IllegalStateException(e); 1444 } 1445 } 1446 1447 CharSequence elementXml = element.toXML(); 1448 if (elementXml instanceof XmlStringBuilder) { 1449 ((XmlStringBuilder) elementXml).write(writer); 1450 } 1451 else { 1452 writer.write(elementXml.toString()); 1453 } 1454 1455 if (queue.isEmpty()) { 1456 writer.flush(); 1457 } 1458 if (packet != null) { 1459 firePacketSendingListeners(packet); 1460 } 1461 } 1462 if (!instantShutdown) { 1463 // Flush out the rest of the queue. 1464 try { 1465 while (!queue.isEmpty()) { 1466 Element packet = queue.remove(); 1467 writer.write(packet.toXML().toString()); 1468 } 1469 writer.flush(); 1470 } 1471 catch (Exception e) { 1472 LOGGER.log(Level.WARNING, 1473 "Exception flushing queue during shutdown, ignore and continue", 1474 e); 1475 } 1476 1477 // Close the stream. 1478 try { 1479 writer.write("</stream:stream>"); 1480 writer.flush(); 1481 } 1482 catch (Exception e) { 1483 LOGGER.log(Level.WARNING, "Exception writing closing stream element", e); 1484 } 1485 1486 // Delete the queue contents (hopefully nothing is left). 1487 queue.clear(); 1488 } else if (instantShutdown && isSmEnabled()) { 1489 // This was an instantShutdown and SM is enabled, drain all remaining stanzas 1490 // into the unacknowledgedStanzas queue 1491 drainWriterQueueToUnacknowledgedStanzas(); 1492 } 1493 // Do *not* close the writer here, as it will cause the socket 1494 // to get closed. But we may want to receive further stanzas 1495 // until the closing stream tag is received. The socket will be 1496 // closed in shutdown(). 1497 } 1498 catch (Exception e) { 1499 // The exception can be ignored if the the connection is 'done' 1500 // or if the it was caused because the socket got closed 1501 if (!(done() || queue.isShutdown())) { 1502 writerException = e; 1503 } else { 1504 LOGGER.log(Level.FINE, "Ignoring Exception in writePackets()", e); 1505 } 1506 } finally { 1507 LOGGER.fine("Reporting shutdownDone success in writer thread"); 1508 shutdownDone.reportSuccess(); 1509 } 1510 // Delay notifyConnectionError after shutdownDone has been reported in the finally block. 1511 if (writerException != null) { 1512 notifyConnectionError(writerException); 1513 } 1514 } 1515 1516 private void drainWriterQueueToUnacknowledgedStanzas() { 1517 List<Element> elements = new ArrayList<Element>(queue.size()); 1518 queue.drainTo(elements); 1519 for (Element element : elements) { 1520 if (element instanceof Stanza) { 1521 unacknowledgedStanzas.add((Stanza) element); 1522 } 1523 } 1524 } 1525 } 1526 1527 /** 1528 * Set if Stream Management should be used by default for new connections. 1529 * 1530 * @param useSmDefault true to use Stream Management for new connections. 1531 */ 1532 public static void setUseStreamManagementDefault(boolean useSmDefault) { 1533 XMPPTCPConnection.useSmDefault = useSmDefault; 1534 } 1535 1536 /** 1537 * Set if Stream Management resumption should be used by default for new connections. 1538 * 1539 * @param useSmResumptionDefault true to use Stream Management resumption for new connections. 1540 * @deprecated use {@link #setUseStreamManagementResumptionDefault(boolean)} instead. 1541 */ 1542 @Deprecated 1543 public static void setUseStreamManagementResumptiodDefault(boolean useSmResumptionDefault) { 1544 setUseStreamManagementResumptionDefault(useSmResumptionDefault); 1545 } 1546 1547 /** 1548 * Set if Stream Management resumption should be used by default for new connections. 1549 * 1550 * @param useSmResumptionDefault true to use Stream Management resumption for new connections. 1551 */ 1552 public static void setUseStreamManagementResumptionDefault(boolean useSmResumptionDefault) { 1553 if (useSmResumptionDefault) { 1554 // Also enable SM is resumption is enabled 1555 setUseStreamManagementDefault(useSmResumptionDefault); 1556 } 1557 XMPPTCPConnection.useSmResumptionDefault = useSmResumptionDefault; 1558 } 1559 1560 /** 1561 * Set if Stream Management should be used if supported by the server. 1562 * 1563 * @param useSm true to use Stream Management. 1564 */ 1565 public void setUseStreamManagement(boolean useSm) { 1566 this.useSm = useSm; 1567 } 1568 1569 /** 1570 * Set if Stream Management resumption should be used if supported by the server. 1571 * 1572 * @param useSmResumption true to use Stream Management resumption. 1573 */ 1574 public void setUseStreamManagementResumption(boolean useSmResumption) { 1575 if (useSmResumption) { 1576 // Also enable SM is resumption is enabled 1577 setUseStreamManagement(useSmResumption); 1578 } 1579 this.useSmResumption = useSmResumption; 1580 } 1581 1582 /** 1583 * Set the preferred resumption time in seconds. 1584 * @param resumptionTime the preferred resumption time in seconds 1585 */ 1586 public void setPreferredResumptionTime(int resumptionTime) { 1587 smClientMaxResumptionTime = resumptionTime; 1588 } 1589 1590 /** 1591 * Add a predicate for Stream Management acknowledgment requests. 1592 * <p> 1593 * Those predicates are used to determine when a Stream Management acknowledgement request is send to the server. 1594 * Some pre-defined predicates are found in the <code>org.jivesoftware.smack.sm.predicates</code> package. 1595 * </p> 1596 * <p> 1597 * If not predicate is configured, the {@link Predicate#forMessagesOrAfter5Stanzas()} will be used. 1598 * </p> 1599 * 1600 * @param predicate the predicate to add. 1601 * @return if the predicate was not already active. 1602 */ 1603 public boolean addRequestAckPredicate(StanzaFilter predicate) { 1604 synchronized (requestAckPredicates) { 1605 return requestAckPredicates.add(predicate); 1606 } 1607 } 1608 1609 /** 1610 * Remove the given predicate for Stream Management acknowledgment request. 1611 * @param predicate the predicate to remove. 1612 * @return true if the predicate was removed. 1613 */ 1614 public boolean removeRequestAckPredicate(StanzaFilter predicate) { 1615 synchronized (requestAckPredicates) { 1616 return requestAckPredicates.remove(predicate); 1617 } 1618 } 1619 1620 /** 1621 * Remove all predicates for Stream Management acknowledgment requests. 1622 */ 1623 public void removeAllRequestAckPredicates() { 1624 synchronized (requestAckPredicates) { 1625 requestAckPredicates.clear(); 1626 } 1627 } 1628 1629 /** 1630 * Send an unconditional Stream Management acknowledgement request to the server. 1631 * 1632 * @throws StreamManagementNotEnabledException if Stream Mangement is not enabled. 1633 * @throws NotConnectedException if the connection is not connected. 1634 * @throws InterruptedException 1635 */ 1636 public void requestSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException, InterruptedException { 1637 if (!isSmEnabled()) { 1638 throw new StreamManagementException.StreamManagementNotEnabledException(); 1639 } 1640 requestSmAcknowledgementInternal(); 1641 } 1642 1643 private void requestSmAcknowledgementInternal() throws NotConnectedException, InterruptedException { 1644 packetWriter.sendStreamElement(AckRequest.INSTANCE); 1645 } 1646 1647 /** 1648 * Send a unconditional Stream Management acknowledgment to the server. 1649 * <p> 1650 * See <a href="http://xmpp.org/extensions/xep-0198.html#acking">XEP-198: Stream Management ยง 4. Acks</a>: 1651 * "Either party MAY send an <a/> element at any time (e.g., after it has received a certain number of stanzas, 1652 * or after a certain period of time), even if it has not received an <r/> element from the other party." 1653 * </p> 1654 * 1655 * @throws StreamManagementNotEnabledException if Stream Management is not enabled. 1656 * @throws NotConnectedException if the connection is not connected. 1657 * @throws InterruptedException 1658 */ 1659 public void sendSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException, InterruptedException { 1660 if (!isSmEnabled()) { 1661 throw new StreamManagementException.StreamManagementNotEnabledException(); 1662 } 1663 sendSmAcknowledgementInternal(); 1664 } 1665 1666 private void sendSmAcknowledgementInternal() throws NotConnectedException, InterruptedException { 1667 packetWriter.sendStreamElement(new AckAnswer(clientHandledStanzasCount)); 1668 } 1669 1670 /** 1671 * Add a Stanza acknowledged listener. 1672 * <p> 1673 * Those listeners will be invoked every time a Stanza has been acknowledged by the server. The will not get 1674 * automatically removed. Consider using {@link #addStanzaIdAcknowledgedListener(String, StanzaListener)} when 1675 * possible. 1676 * </p> 1677 * 1678 * @param listener the listener to add. 1679 */ 1680 public void addStanzaAcknowledgedListener(StanzaListener listener) { 1681 stanzaAcknowledgedListeners.add(listener); 1682 } 1683 1684 /** 1685 * Remove the given Stanza acknowledged listener. 1686 * 1687 * @param listener the listener. 1688 * @return true if the listener was removed. 1689 */ 1690 public boolean removeStanzaAcknowledgedListener(StanzaListener listener) { 1691 return stanzaAcknowledgedListeners.remove(listener); 1692 } 1693 1694 /** 1695 * Remove all stanza acknowledged listeners. 1696 */ 1697 public void removeAllStanzaAcknowledgedListeners() { 1698 stanzaAcknowledgedListeners.clear(); 1699 } 1700 1701 /** 1702 * Add a new Stanza ID acknowledged listener for the given ID. 1703 * <p> 1704 * The listener will be invoked if the stanza with the given ID was acknowledged by the server. It will 1705 * automatically be removed after the listener was run. 1706 * </p> 1707 * 1708 * @param id the stanza ID. 1709 * @param listener the listener to invoke. 1710 * @return the previous listener for this stanza ID or null. 1711 * @throws StreamManagementNotEnabledException if Stream Management is not enabled. 1712 */ 1713 public StanzaListener addStanzaIdAcknowledgedListener(final String id, StanzaListener listener) throws StreamManagementNotEnabledException { 1714 // Prevent users from adding callbacks that will never get removed 1715 if (!smWasEnabledAtLeastOnce) { 1716 throw new StreamManagementException.StreamManagementNotEnabledException(); 1717 } 1718 // Remove the listener after max. 12 hours 1719 final int removeAfterSeconds = Math.min(getMaxSmResumptionTime(), 12 * 60 * 60); 1720 schedule(new Runnable() { 1721 @Override 1722 public void run() { 1723 stanzaIdAcknowledgedListeners.remove(id); 1724 } 1725 }, removeAfterSeconds, TimeUnit.SECONDS); 1726 return stanzaIdAcknowledgedListeners.put(id, listener); 1727 } 1728 1729 /** 1730 * Remove the Stanza ID acknowledged listener for the given ID. 1731 * 1732 * @param id the stanza ID. 1733 * @return true if the listener was found and removed, false otherwise. 1734 */ 1735 public StanzaListener removeStanzaIdAcknowledgedListener(String id) { 1736 return stanzaIdAcknowledgedListeners.remove(id); 1737 } 1738 1739 /** 1740 * Removes all Stanza ID acknowledged listeners. 1741 */ 1742 public void removeAllStanzaIdAcknowledgedListeners() { 1743 stanzaIdAcknowledgedListeners.clear(); 1744 } 1745 1746 /** 1747 * Returns true if Stream Management is supported by the server. 1748 * 1749 * @return true if Stream Management is supported by the server. 1750 */ 1751 public boolean isSmAvailable() { 1752 return hasFeature(StreamManagementFeature.ELEMENT, StreamManagement.NAMESPACE); 1753 } 1754 1755 /** 1756 * Returns true if Stream Management was successfully negotiated with the server. 1757 * 1758 * @return true if Stream Management was negotiated. 1759 */ 1760 public boolean isSmEnabled() { 1761 return smEnabledSyncPoint.wasSuccessful(); 1762 } 1763 1764 /** 1765 * Returns true if the stream was successfully resumed with help of Stream Management. 1766 * 1767 * @return true if the stream was resumed. 1768 */ 1769 public boolean streamWasResumed() { 1770 return smResumedSyncPoint.wasSuccessful(); 1771 } 1772 1773 /** 1774 * Returns true if the connection is disconnected by a Stream resumption via Stream Management is possible. 1775 * 1776 * @return true if disconnected but resumption possible. 1777 */ 1778 public boolean isDisconnectedButSmResumptionPossible() { 1779 return disconnectedButResumeable && isSmResumptionPossible(); 1780 } 1781 1782 /** 1783 * Returns true if the stream is resumable. 1784 * 1785 * @return true if the stream is resumable. 1786 */ 1787 public boolean isSmResumptionPossible() { 1788 // There is no resumable stream available 1789 if (smSessionId == null) 1790 return false; 1791 1792 final Long shutdownTimestamp = packetWriter.shutdownTimestamp; 1793 // Seems like we are already reconnected, report true 1794 if (shutdownTimestamp == null) { 1795 return true; 1796 } 1797 1798 // See if resumption time is over 1799 long current = System.currentTimeMillis(); 1800 long maxResumptionMillies = ((long) getMaxSmResumptionTime()) * 1000; 1801 if (current > shutdownTimestamp + maxResumptionMillies) { 1802 // Stream resumption is *not* possible if the current timestamp is greater then the greatest timestamp where 1803 // resumption is possible 1804 return false; 1805 } else { 1806 return true; 1807 } 1808 } 1809 1810 /** 1811 * Drop the stream management state. Sets {@link #smSessionId} and 1812 * {@link #unacknowledgedStanzas} to <code>null</code>. 1813 */ 1814 private void dropSmState() { 1815 // clientHandledCount and serverHandledCount will be reset on <enable/> and <enabled/> 1816 // respective. No need to reset them here. 1817 smSessionId = null; 1818 unacknowledgedStanzas = null; 1819 } 1820 1821 /** 1822 * Get the maximum resumption time in seconds after which a managed stream can be resumed. 1823 * <p> 1824 * This method will return {@link Integer#MAX_VALUE} if neither the client nor the server specify a maximum 1825 * resumption time. Be aware of integer overflows when using this value, e.g. do not add arbitrary values to it 1826 * without checking for overflows before. 1827 * </p> 1828 * 1829 * @return the maximum resumption time in seconds or {@link Integer#MAX_VALUE} if none set. 1830 */ 1831 public int getMaxSmResumptionTime() { 1832 int clientResumptionTime = smClientMaxResumptionTime > 0 ? smClientMaxResumptionTime : Integer.MAX_VALUE; 1833 int serverResumptionTime = smServerMaxResumptimTime > 0 ? smServerMaxResumptimTime : Integer.MAX_VALUE; 1834 return Math.min(clientResumptionTime, serverResumptionTime); 1835 } 1836 1837 private void processHandledCount(long handledCount) throws StreamManagementCounterError { 1838 long ackedStanzasCount = SMUtils.calculateDelta(handledCount, serverHandledStanzasCount); 1839 final List<Stanza> ackedStanzas = new ArrayList<Stanza>( 1840 ackedStanzasCount <= Integer.MAX_VALUE ? (int) ackedStanzasCount 1841 : Integer.MAX_VALUE); 1842 for (long i = 0; i < ackedStanzasCount; i++) { 1843 Stanza ackedStanza = unacknowledgedStanzas.poll(); 1844 // If the server ack'ed a stanza, then it must be in the 1845 // unacknowledged stanza queue. There can be no exception. 1846 if (ackedStanza == null) { 1847 throw new StreamManagementCounterError(handledCount, serverHandledStanzasCount, 1848 ackedStanzasCount, ackedStanzas); 1849 } 1850 ackedStanzas.add(ackedStanza); 1851 } 1852 1853 boolean atLeastOneStanzaAcknowledgedListener = false; 1854 if (!stanzaAcknowledgedListeners.isEmpty()) { 1855 // If stanzaAcknowledgedListeners is not empty, the we have at least one 1856 atLeastOneStanzaAcknowledgedListener = true; 1857 } 1858 else { 1859 // Otherwise we look for a matching id in the stanza *id* acknowledged listeners 1860 for (Stanza ackedStanza : ackedStanzas) { 1861 String id = ackedStanza.getStanzaId(); 1862 if (id != null && stanzaIdAcknowledgedListeners.containsKey(id)) { 1863 atLeastOneStanzaAcknowledgedListener = true; 1864 break; 1865 } 1866 } 1867 } 1868 1869 // Only spawn a new thread if there is a chance that some listener is invoked 1870 if (atLeastOneStanzaAcknowledgedListener) { 1871 asyncGo(new Runnable() { 1872 @Override 1873 public void run() { 1874 for (Stanza ackedStanza : ackedStanzas) { 1875 for (StanzaListener listener : stanzaAcknowledgedListeners) { 1876 try { 1877 listener.processStanza(ackedStanza); 1878 } 1879 catch (InterruptedException | NotConnectedException e) { 1880 LOGGER.log(Level.FINER, "Received exception", e); 1881 } 1882 } 1883 String id = ackedStanza.getStanzaId(); 1884 if (StringUtils.isNullOrEmpty(id)) { 1885 continue; 1886 } 1887 StanzaListener listener = stanzaIdAcknowledgedListeners.remove(id); 1888 if (listener != null) { 1889 try { 1890 listener.processStanza(ackedStanza); 1891 } 1892 catch (InterruptedException | NotConnectedException e) { 1893 LOGGER.log(Level.FINER, "Received exception", e); 1894 } 1895 } 1896 } 1897 } 1898 }); 1899 } 1900 1901 serverHandledStanzasCount = handledCount; 1902 } 1903 1904 /** 1905 * Set the default bundle and defer callback used for new connections. 1906 * 1907 * @param defaultBundleAndDeferCallback 1908 * @see BundleAndDeferCallback 1909 * @since 4.1 1910 */ 1911 public static void setDefaultBundleAndDeferCallback(BundleAndDeferCallback defaultBundleAndDeferCallback) { 1912 XMPPTCPConnection.defaultBundleAndDeferCallback = defaultBundleAndDeferCallback; 1913 } 1914 1915 /** 1916 * Set the bundle and defer callback used for this connection. 1917 * <p> 1918 * You can use <code>null</code> as argument to reset the callback. Outgoing stanzas will then 1919 * no longer get deferred. 1920 * </p> 1921 * 1922 * @param bundleAndDeferCallback the callback or <code>null</code>. 1923 * @see BundleAndDeferCallback 1924 * @since 4.1 1925 */ 1926 public void setBundleandDeferCallback(BundleAndDeferCallback bundleAndDeferCallback) { 1927 this.bundleAndDeferCallback = bundleAndDeferCallback; 1928 } 1929 1930}