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