001/** 002 * 003 * Copyright 2009 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; 018 019import java.io.IOException; 020import java.io.Reader; 021import java.io.Writer; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.LinkedHashMap; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.concurrent.ConcurrentLinkedQueue; 031import java.util.concurrent.CopyOnWriteArraySet; 032import java.util.concurrent.ExecutorService; 033import java.util.concurrent.Executors; 034import java.util.concurrent.LinkedBlockingQueue; 035import java.util.concurrent.ScheduledExecutorService; 036import java.util.concurrent.ScheduledFuture; 037import java.util.concurrent.ThreadFactory; 038import java.util.concurrent.ThreadPoolExecutor; 039import java.util.concurrent.TimeUnit; 040import java.util.concurrent.atomic.AtomicInteger; 041import java.util.concurrent.locks.Lock; 042import java.util.concurrent.locks.ReentrantLock; 043import java.util.logging.Level; 044import java.util.logging.Logger; 045 046import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; 047import org.jivesoftware.smack.SmackConfiguration.UnknownIqRequestReplyMode; 048import org.jivesoftware.smack.SmackException.AlreadyConnectedException; 049import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; 050import org.jivesoftware.smack.SmackException.NoResponseException; 051import org.jivesoftware.smack.SmackException.NotConnectedException; 052import org.jivesoftware.smack.SmackException.NotLoggedInException; 053import org.jivesoftware.smack.SmackException.ResourceBindingNotOfferedException; 054import org.jivesoftware.smack.SmackException.SecurityRequiredByClientException; 055import org.jivesoftware.smack.SmackException.SecurityRequiredException; 056import org.jivesoftware.smack.XMPPException.StreamErrorException; 057import org.jivesoftware.smack.XMPPException.XMPPErrorException; 058import org.jivesoftware.smack.compress.packet.Compress; 059import org.jivesoftware.smack.compression.XMPPInputOutputStream; 060import org.jivesoftware.smack.debugger.SmackDebugger; 061import org.jivesoftware.smack.filter.IQReplyFilter; 062import org.jivesoftware.smack.filter.StanzaFilter; 063import org.jivesoftware.smack.filter.StanzaIdFilter; 064import org.jivesoftware.smack.iqrequest.IQRequestHandler; 065import org.jivesoftware.smack.packet.Bind; 066import org.jivesoftware.smack.packet.ErrorIQ; 067import org.jivesoftware.smack.packet.ExtensionElement; 068import org.jivesoftware.smack.packet.IQ; 069import org.jivesoftware.smack.packet.Mechanisms; 070import org.jivesoftware.smack.packet.Message; 071import org.jivesoftware.smack.packet.Nonza; 072import org.jivesoftware.smack.packet.Presence; 073import org.jivesoftware.smack.packet.Session; 074import org.jivesoftware.smack.packet.Stanza; 075import org.jivesoftware.smack.packet.StartTls; 076import org.jivesoftware.smack.packet.StreamError; 077import org.jivesoftware.smack.packet.XMPPError; 078import org.jivesoftware.smack.parsing.ParsingExceptionCallback; 079import org.jivesoftware.smack.provider.ExtensionElementProvider; 080import org.jivesoftware.smack.provider.ProviderManager; 081import org.jivesoftware.smack.sasl.core.SASLAnonymous; 082import org.jivesoftware.smack.util.Async; 083import org.jivesoftware.smack.util.DNSUtil; 084import org.jivesoftware.smack.util.Objects; 085import org.jivesoftware.smack.util.PacketParserUtils; 086import org.jivesoftware.smack.util.ParserUtils; 087import org.jivesoftware.smack.util.SmackExecutorThreadFactory; 088import org.jivesoftware.smack.util.StringUtils; 089import org.jivesoftware.smack.util.dns.HostAddress; 090 091import org.jxmpp.jid.DomainBareJid; 092import org.jxmpp.jid.EntityFullJid; 093import org.jxmpp.jid.Jid; 094import org.jxmpp.jid.parts.Resourcepart; 095import org.jxmpp.util.XmppStringUtils; 096import org.xmlpull.v1.XmlPullParser; 097 098 099public abstract class AbstractXMPPConnection implements XMPPConnection { 100 private static final Logger LOGGER = Logger.getLogger(AbstractXMPPConnection.class.getName()); 101 102 /** 103 * Counter to uniquely identify connections that are created. 104 */ 105 private final static AtomicInteger connectionCounter = new AtomicInteger(0); 106 107 static { 108 // Ensure the SmackConfiguration class is loaded by calling a method in it. 109 SmackConfiguration.getVersion(); 110 } 111 112 /** 113 * A collection of ConnectionListeners which listen for connection closing 114 * and reconnection events. 115 */ 116 protected final Set<ConnectionListener> connectionListeners = 117 new CopyOnWriteArraySet<ConnectionListener>(); 118 119 /** 120 * A collection of StanzaCollectors which collects packets for a specified filter 121 * and perform blocking and polling operations on the result queue. 122 * <p> 123 * We use a ConcurrentLinkedQueue here, because its Iterator is weakly 124 * consistent and we want {@link #invokeStanzaCollectorsAndNotifyRecvListeners(Stanza)} for-each 125 * loop to be lock free. As drawback, removing a StanzaCollector is O(n). 126 * The alternative would be a synchronized HashSet, but this would mean a 127 * synchronized block around every usage of <code>collectors</code>. 128 * </p> 129 */ 130 private final Collection<StanzaCollector> collectors = new ConcurrentLinkedQueue<>(); 131 132 /** 133 * List of PacketListeners that will be notified synchronously when a new stanza(/packet) was received. 134 */ 135 private final Map<StanzaListener, ListenerWrapper> syncRecvListeners = new LinkedHashMap<>(); 136 137 /** 138 * List of PacketListeners that will be notified asynchronously when a new stanza(/packet) was received. 139 */ 140 private final Map<StanzaListener, ListenerWrapper> asyncRecvListeners = new LinkedHashMap<>(); 141 142 /** 143 * List of PacketListeners that will be notified when a new stanza(/packet) was sent. 144 */ 145 private final Map<StanzaListener, ListenerWrapper> sendListeners = 146 new HashMap<StanzaListener, ListenerWrapper>(); 147 148 /** 149 * List of PacketListeners that will be notified when a new stanza(/packet) is about to be 150 * sent to the server. These interceptors may modify the stanza(/packet) before it is being 151 * actually sent to the server. 152 */ 153 private final Map<StanzaListener, InterceptorWrapper> interceptors = 154 new HashMap<StanzaListener, InterceptorWrapper>(); 155 156 protected final Lock connectionLock = new ReentrantLock(); 157 158 protected final Map<String, ExtensionElement> streamFeatures = new HashMap<String, ExtensionElement>(); 159 160 /** 161 * The full JID of the authenticated user, as returned by the resource binding response of the server. 162 * <p> 163 * It is important that we don't infer the user from the login() arguments and the configurations service name, as, 164 * for example, when SASL External is used, the username is not given to login but taken from the 'external' 165 * certificate. 166 * </p> 167 */ 168 protected EntityFullJid user; 169 170 protected boolean connected = false; 171 172 /** 173 * The stream ID, see RFC 6120 § 4.7.3 174 */ 175 protected String streamId; 176 177 /** 178 * The timeout to wait for a reply in milliseconds. 179 */ 180 private long replyTimeout = SmackConfiguration.getDefaultReplyTimeout(); 181 182 /** 183 * The SmackDebugger allows to log and debug XML traffic. 184 */ 185 protected SmackDebugger debugger = null; 186 187 /** 188 * The Reader which is used for the debugger. 189 */ 190 protected Reader reader; 191 192 /** 193 * The Writer which is used for the debugger. 194 */ 195 protected Writer writer; 196 197 protected final SynchronizationPoint<SmackException> tlsHandled = new SynchronizationPoint<>(this, "establishing TLS"); 198 199 /** 200 * Set to success if the last features stanza from the server has been parsed. A XMPP connection 201 * handshake can invoke multiple features stanzas, e.g. when TLS is activated a second feature 202 * stanza is send by the server. This is set to true once the last feature stanza has been 203 * parsed. 204 */ 205 protected final SynchronizationPoint<Exception> lastFeaturesReceived = new SynchronizationPoint<Exception>( 206 AbstractXMPPConnection.this, "last stream features received from server"); 207 208 /** 209 * Set to success if the SASL feature has been received. 210 */ 211 protected final SynchronizationPoint<XMPPException> saslFeatureReceived = new SynchronizationPoint<>( 212 AbstractXMPPConnection.this, "SASL mechanisms stream feature from server"); 213 214 /** 215 * The SASLAuthentication manager that is responsible for authenticating with the server. 216 */ 217 protected final SASLAuthentication saslAuthentication; 218 219 /** 220 * A number to uniquely identify connections that are created. This is distinct from the 221 * connection ID, which is a value sent by the server once a connection is made. 222 */ 223 protected final int connectionCounterValue = connectionCounter.getAndIncrement(); 224 225 /** 226 * Holds the initial configuration used while creating the connection. 227 */ 228 protected final ConnectionConfiguration config; 229 230 /** 231 * Defines how the from attribute of outgoing stanzas should be handled. 232 */ 233 private FromMode fromMode = FromMode.OMITTED; 234 235 protected XMPPInputOutputStream compressionHandler; 236 237 private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration.getDefaultParsingExceptionCallback(); 238 239 /** 240 * This scheduled thread pool executor is used to remove pending callbacks. 241 */ 242 protected static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor( 243 new ThreadFactory() { 244 @Override 245 public Thread newThread(Runnable runnable) { 246 Thread thread = new Thread(runnable); 247 thread.setName("Smack Scheduled Executor Service"); 248 thread.setDaemon(true); 249 return thread; 250 } 251 }); 252 253 /** 254 * A cached thread pool executor service with custom thread factory to set meaningful names on the threads and set 255 * them 'daemon'. 256 */ 257 private static final ExecutorService CACHED_EXECUTOR_SERVICE = Executors.newCachedThreadPool(new ThreadFactory() { 258 @Override 259 public Thread newThread(Runnable runnable) { 260 Thread thread = new Thread(runnable); 261 thread.setName("Smack Cached Executor"); 262 thread.setDaemon(true); 263 return thread; 264 } 265 }); 266 267 /** 268 * A executor service used to invoke the callbacks of synchronous stanza(/packet) listeners. We use a executor service to 269 * decouple incoming stanza processing from callback invocation. It is important that order of callback invocation 270 * is the same as the order of the incoming stanzas. Therefore we use a <i>single</i> threaded executor service. 271 */ 272 private final ExecutorService singleThreadedExecutorService = new ThreadPoolExecutor(0, 1, 30L, 273 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), 274 new SmackExecutorThreadFactory(this, "Single Threaded Executor")); 275 276 /** 277 * The used host to establish the connection to 278 */ 279 protected String host; 280 281 /** 282 * The used port to establish the connection to 283 */ 284 protected int port; 285 286 /** 287 * Flag that indicates if the user is currently authenticated with the server. 288 */ 289 protected boolean authenticated = false; 290 291 /** 292 * Flag that indicates if the user was authenticated with the server when the connection 293 * to the server was closed (abruptly or not). 294 */ 295 protected boolean wasAuthenticated = false; 296 297 private final Map<String, IQRequestHandler> setIqRequestHandler = new HashMap<>(); 298 private final Map<String, IQRequestHandler> getIqRequestHandler = new HashMap<>(); 299 300 /** 301 * Create a new XMPPConnection to an XMPP server. 302 * 303 * @param configuration The configuration which is used to establish the connection. 304 */ 305 protected AbstractXMPPConnection(ConnectionConfiguration configuration) { 306 saslAuthentication = new SASLAuthentication(this, configuration); 307 config = configuration; 308 // Notify listeners that a new connection has been established 309 for (ConnectionCreationListener listener : XMPPConnectionRegistry.getConnectionCreationListeners()) { 310 listener.connectionCreated(this); 311 } 312 } 313 314 /** 315 * Get the connection configuration used by this connection. 316 * 317 * @return the connection configuration. 318 */ 319 public ConnectionConfiguration getConfiguration() { 320 return config; 321 } 322 323 @SuppressWarnings("deprecation") 324 @Override 325 public DomainBareJid getServiceName() { 326 return getXMPPServiceDomain(); 327 } 328 329 @Override 330 public DomainBareJid getXMPPServiceDomain() { 331 if (xmppServiceDomain != null) { 332 return xmppServiceDomain; 333 } 334 return config.getXMPPServiceDomain(); 335 } 336 337 @Override 338 public String getHost() { 339 return host; 340 } 341 342 @Override 343 public int getPort() { 344 return port; 345 } 346 347 @Override 348 public abstract boolean isSecureConnection(); 349 350 protected abstract void sendStanzaInternal(Stanza packet) throws NotConnectedException, InterruptedException; 351 352 @Override 353 public abstract void sendNonza(Nonza element) throws NotConnectedException, InterruptedException; 354 355 @Override 356 public abstract boolean isUsingCompression(); 357 358 /** 359 * Establishes a connection to the XMPP server. It basically 360 * creates and maintains a connection to the server. 361 * <p> 362 * Listeners will be preserved from a previous connection. 363 * </p> 364 * 365 * @throws XMPPException if an error occurs on the XMPP protocol level. 366 * @throws SmackException if an error occurs somewhere else besides XMPP protocol level. 367 * @throws IOException 368 * @return a reference to this object, to chain <code>connect()</code> with <code>login()</code>. 369 * @throws InterruptedException 370 */ 371 public synchronized AbstractXMPPConnection connect() throws SmackException, IOException, XMPPException, InterruptedException { 372 // Check if not already connected 373 throwAlreadyConnectedExceptionIfAppropriate(); 374 375 // Reset the connection state 376 saslAuthentication.init(); 377 saslFeatureReceived.init(); 378 lastFeaturesReceived.init(); 379 tlsHandled.init(); 380 streamId = null; 381 382 // Perform the actual connection to the XMPP service 383 connectInternal(); 384 385 // If TLS is required but the server doesn't offer it, disconnect 386 // from the server and throw an error. First check if we've already negotiated TLS 387 // and are secure, however (features get parsed a second time after TLS is established). 388 if (!isSecureConnection() && getConfiguration().getSecurityMode() == SecurityMode.required) { 389 shutdown(); 390 throw new SecurityRequiredByClientException(); 391 } 392 393 // Make note of the fact that we're now connected. 394 connected = true; 395 callConnectionConnectedListener(); 396 397 return this; 398 } 399 400 /** 401 * Abstract method that concrete subclasses of XMPPConnection need to implement to perform their 402 * way of XMPP connection establishment. Implementations are required to perform an automatic 403 * login if the previous connection state was logged (authenticated). 404 * 405 * @throws SmackException 406 * @throws IOException 407 * @throws XMPPException 408 * @throws InterruptedException 409 */ 410 protected abstract void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException; 411 412 private String usedUsername, usedPassword; 413 414 /** 415 * The resourcepart used for this connection. May not be the resulting resourcepart if it's null or overridden by the XMPP service. 416 */ 417 private Resourcepart usedResource; 418 419 /** 420 * Logs in to the server using the strongest SASL mechanism supported by 421 * the server. If more than the connection's default stanza(/packet) timeout elapses in each step of the 422 * authentication process without a response from the server, a 423 * {@link SmackException.NoResponseException} will be thrown. 424 * <p> 425 * Before logging in (i.e. authenticate) to the server the connection must be connected 426 * by calling {@link #connect}. 427 * </p> 428 * <p> 429 * It is possible to log in without sending an initial available presence by using 430 * {@link ConnectionConfiguration.Builder#setSendPresence(boolean)}. 431 * Finally, if you want to not pass a password and instead use a more advanced mechanism 432 * while using SASL then you may be interested in using 433 * {@link ConnectionConfiguration.Builder#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}. 434 * For more advanced login settings see {@link ConnectionConfiguration}. 435 * </p> 436 * 437 * @throws XMPPException if an error occurs on the XMPP protocol level. 438 * @throws SmackException if an error occurs somewhere else besides XMPP protocol level. 439 * @throws IOException if an I/O error occurs during login. 440 * @throws InterruptedException 441 */ 442 public synchronized void login() throws XMPPException, SmackException, IOException, InterruptedException { 443 // The previously used username, password and resource take over precedence over the 444 // ones from the connection configuration 445 CharSequence username = usedUsername != null ? usedUsername : config.getUsername(); 446 String password = usedPassword != null ? usedPassword : config.getPassword(); 447 Resourcepart resource = usedResource != null ? usedResource : config.getResource(); 448 login(username, password, resource); 449 } 450 451 /** 452 * Same as {@link #login(CharSequence, String, Resourcepart)}, but takes the resource from the connection 453 * configuration. 454 * 455 * @param username 456 * @param password 457 * @throws XMPPException 458 * @throws SmackException 459 * @throws IOException 460 * @throws InterruptedException 461 * @see #login 462 */ 463 public synchronized void login(CharSequence username, String password) throws XMPPException, SmackException, 464 IOException, InterruptedException { 465 login(username, password, config.getResource()); 466 } 467 468 /** 469 * Login with the given username (authorization identity). You may omit the password if a callback handler is used. 470 * If resource is null, then the server will generate one. 471 * 472 * @param username 473 * @param password 474 * @param resource 475 * @throws XMPPException 476 * @throws SmackException 477 * @throws IOException 478 * @throws InterruptedException 479 * @see #login 480 */ 481 public synchronized void login(CharSequence username, String password, Resourcepart resource) throws XMPPException, 482 SmackException, IOException, InterruptedException { 483 if (!config.allowNullOrEmptyUsername) { 484 StringUtils.requireNotNullOrEmpty(username, "Username must not be null or empty"); 485 } 486 throwNotConnectedExceptionIfAppropriate("Did you call connect() before login()?"); 487 throwAlreadyLoggedInExceptionIfAppropriate(); 488 usedUsername = username != null ? username.toString() : null; 489 usedPassword = password; 490 usedResource = resource; 491 loginInternal(usedUsername, usedPassword, usedResource); 492 } 493 494 protected abstract void loginInternal(String username, String password, Resourcepart resource) 495 throws XMPPException, SmackException, IOException, InterruptedException; 496 497 @Override 498 public final boolean isConnected() { 499 return connected; 500 } 501 502 @Override 503 public final boolean isAuthenticated() { 504 return authenticated; 505 } 506 507 @Override 508 public final EntityFullJid getUser() { 509 return user; 510 } 511 512 @Override 513 public String getStreamId() { 514 if (!isConnected()) { 515 return null; 516 } 517 return streamId; 518 } 519 520 protected void bindResourceAndEstablishSession(Resourcepart resource) throws XMPPErrorException, 521 SmackException, InterruptedException { 522 523 // Wait until either: 524 // - the servers last features stanza has been parsed 525 // - the timeout occurs 526 LOGGER.finer("Waiting for last features to be received before continuing with resource binding"); 527 lastFeaturesReceived.checkIfSuccessOrWait(); 528 529 530 if (!hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { 531 // Server never offered resource binding, which is REQURIED in XMPP client and 532 // server implementations as per RFC6120 7.2 533 throw new ResourceBindingNotOfferedException(); 534 } 535 536 // Resource binding, see RFC6120 7. 537 // Note that we can not use IQReplyFilter here, since the users full JID is not yet 538 // available. It will become available right after the resource has been successfully bound. 539 Bind bindResource = Bind.newSet(resource); 540 StanzaCollector packetCollector = createStanzaCollectorAndSend(new StanzaIdFilter(bindResource), bindResource); 541 Bind response = packetCollector.nextResultOrThrow(); 542 // Set the connections user to the result of resource binding. It is important that we don't infer the user 543 // from the login() arguments and the configurations service name, as, for example, when SASL External is used, 544 // the username is not given to login but taken from the 'external' certificate. 545 user = response.getJid(); 546 xmppServiceDomain = user.asDomainBareJid(); 547 548 Session.Feature sessionFeature = getFeature(Session.ELEMENT, Session.NAMESPACE); 549 // Only bind the session if it's announced as stream feature by the server, is not optional and not disabled 550 // For more information see http://tools.ietf.org/html/draft-cridland-xmpp-session-01 551 // TODO remove this suppression once "disable legacy session" code has been removed from Smack 552 @SuppressWarnings("deprecation") 553 boolean legacySessionDisabled = getConfiguration().isLegacySessionDisabled(); 554 if (sessionFeature != null && !sessionFeature.isOptional() && !legacySessionDisabled) { 555 Session session = new Session(); 556 packetCollector = createStanzaCollectorAndSend(new StanzaIdFilter(session), session); 557 packetCollector.nextResultOrThrow(); 558 } 559 } 560 561 protected void afterSuccessfulLogin(final boolean resumed) throws NotConnectedException, InterruptedException { 562 // Indicate that we're now authenticated. 563 this.authenticated = true; 564 565 // If debugging is enabled, change the the debug window title to include the 566 // name we are now logged-in as. 567 // If DEBUG was set to true AFTER the connection was created the debugger 568 // will be null 569 if (config.isDebuggerEnabled() && debugger != null) { 570 debugger.userHasLogged(user); 571 } 572 callConnectionAuthenticatedListener(resumed); 573 574 // Set presence to online. It is important that this is done after 575 // callConnectionAuthenticatedListener(), as this call will also 576 // eventually load the roster. And we should load the roster before we 577 // send the initial presence. 578 if (config.isSendPresence() && !resumed) { 579 sendStanza(new Presence(Presence.Type.available)); 580 } 581 } 582 583 @Override 584 public final boolean isAnonymous() { 585 return isAuthenticated() && SASLAnonymous.NAME.equals(getUsedSaslMechansism()); 586 } 587 588 /** 589 * Get the name of the SASL mechanism that was used to authenticate this connection. This returns the name of 590 * mechanism which was used the last time this conneciton was authenticated, and will return <code>null</code> if 591 * this connection was not authenticated before. 592 * 593 * @return the name of the used SASL mechanism. 594 * @since 4.2 595 */ 596 public final String getUsedSaslMechansism() { 597 return saslAuthentication.getNameOfLastUsedSaslMechansism(); 598 } 599 600 private DomainBareJid xmppServiceDomain; 601 602 protected List<HostAddress> hostAddresses; 603 604 /** 605 * Populates {@link #hostAddresses} with the resolved addresses or with the configured host address. If no host 606 * address was configured and all lookups failed, for example with NX_DOMAIN, then {@link #hostAddresses} will be 607 * populated with the empty list. 608 * 609 * @return a list of host addresses where DNS (SRV) RR resolution failed. 610 */ 611 protected List<HostAddress> populateHostAddresses() { 612 List<HostAddress> failedAddresses = new LinkedList<>(); 613 if (config.hostAddress != null) { 614 hostAddresses = new ArrayList<>(1); 615 HostAddress hostAddress = new HostAddress(config.port, config.hostAddress); 616 hostAddresses.add(hostAddress); 617 } 618 else if (config.host != null) { 619 hostAddresses = new ArrayList<HostAddress>(1); 620 HostAddress hostAddress = DNSUtil.getDNSResolver().lookupHostAddress(config.host, config.port, failedAddresses, config.getDnssecMode()); 621 if (hostAddress != null) { 622 hostAddresses.add(hostAddress); 623 } 624 } else { 625 // N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName 626 hostAddresses = DNSUtil.resolveXMPPServiceDomain(config.getXMPPServiceDomain().toString(), failedAddresses, config.getDnssecMode()); 627 } 628 // Either the populated host addresses are not empty *or* there must be at least one failed address. 629 assert (!hostAddresses.isEmpty() || !failedAddresses.isEmpty()); 630 return failedAddresses; 631 } 632 633 protected Lock getConnectionLock() { 634 return connectionLock; 635 } 636 637 protected void throwNotConnectedExceptionIfAppropriate() throws NotConnectedException { 638 throwNotConnectedExceptionIfAppropriate(null); 639 } 640 641 protected void throwNotConnectedExceptionIfAppropriate(String optionalHint) throws NotConnectedException { 642 if (!isConnected()) { 643 throw new NotConnectedException(optionalHint); 644 } 645 } 646 647 protected void throwAlreadyConnectedExceptionIfAppropriate() throws AlreadyConnectedException { 648 if (isConnected()) { 649 throw new AlreadyConnectedException(); 650 } 651 } 652 653 protected void throwAlreadyLoggedInExceptionIfAppropriate() throws AlreadyLoggedInException { 654 if (isAuthenticated()) { 655 throw new AlreadyLoggedInException(); 656 } 657 } 658 659 @Deprecated 660 @Override 661 public void sendPacket(Stanza packet) throws NotConnectedException, InterruptedException { 662 sendStanza(packet); 663 } 664 665 @Override 666 public void sendStanza(Stanza stanza) throws NotConnectedException, InterruptedException { 667 Objects.requireNonNull(stanza, "Stanza must not be null"); 668 assert (stanza instanceof Message || stanza instanceof Presence || stanza instanceof IQ); 669 670 throwNotConnectedExceptionIfAppropriate(); 671 switch (fromMode) { 672 case OMITTED: 673 stanza.setFrom((Jid) null); 674 break; 675 case USER: 676 stanza.setFrom(getUser()); 677 break; 678 case UNCHANGED: 679 default: 680 break; 681 } 682 // Invoke interceptors for the new stanza that is about to be sent. Interceptors may modify 683 // the content of the stanza. 684 firePacketInterceptors(stanza); 685 sendStanzaInternal(stanza); 686 } 687 688 /** 689 * Returns the SASLAuthentication manager that is responsible for authenticating with 690 * the server. 691 * 692 * @return the SASLAuthentication manager that is responsible for authenticating with 693 * the server. 694 */ 695 protected SASLAuthentication getSASLAuthentication() { 696 return saslAuthentication; 697 } 698 699 /** 700 * Closes the connection by setting presence to unavailable then closing the connection to 701 * the XMPP server. The XMPPConnection can still be used for connecting to the server 702 * again. 703 * 704 */ 705 public void disconnect() { 706 Presence unavailablePresence = null; 707 if (isAuthenticated()) { 708 unavailablePresence = new Presence(Presence.Type.unavailable); 709 } 710 try { 711 disconnect(unavailablePresence); 712 } 713 catch (NotConnectedException e) { 714 LOGGER.log(Level.FINEST, "Connection is already disconnected", e); 715 } 716 } 717 718 /** 719 * Closes the connection. A custom unavailable presence is sent to the server, followed 720 * by closing the stream. The XMPPConnection can still be used for connecting to the server 721 * again. A custom unavailable presence is useful for communicating offline presence 722 * information such as "On vacation". Typically, just the status text of the presence 723 * stanza(/packet) is set with online information, but most XMPP servers will deliver the full 724 * presence stanza(/packet) with whatever data is set. 725 * 726 * @param unavailablePresence the optional presence stanza to send during shutdown. 727 * @throws NotConnectedException 728 */ 729 public synchronized void disconnect(Presence unavailablePresence) throws NotConnectedException { 730 if (unavailablePresence != null) { 731 try { 732 sendStanza(unavailablePresence); 733 } catch (InterruptedException e) { 734 LOGGER.log(Level.FINE, 735 "Was interrupted while sending unavailable presence. Continuing to disconnect the connection", 736 e); 737 } 738 } 739 shutdown(); 740 callConnectionClosedListener(); 741 } 742 743 /** 744 * Shuts the current connection down. 745 */ 746 protected abstract void shutdown(); 747 748 @Override 749 public void addConnectionListener(ConnectionListener connectionListener) { 750 if (connectionListener == null) { 751 return; 752 } 753 connectionListeners.add(connectionListener); 754 } 755 756 @Override 757 public void removeConnectionListener(ConnectionListener connectionListener) { 758 connectionListeners.remove(connectionListener); 759 } 760 761 @Override 762 public StanzaCollector createStanzaCollectorAndSend(IQ packet) throws NotConnectedException, InterruptedException { 763 StanzaFilter packetFilter = new IQReplyFilter(packet, this); 764 // Create the packet collector before sending the packet 765 StanzaCollector packetCollector = createStanzaCollectorAndSend(packetFilter, packet); 766 return packetCollector; 767 } 768 769 @Override 770 public StanzaCollector createStanzaCollectorAndSend(StanzaFilter packetFilter, Stanza packet) 771 throws NotConnectedException, InterruptedException { 772 // Create the packet collector before sending the packet 773 StanzaCollector packetCollector = createStanzaCollector(packetFilter); 774 try { 775 // Now we can send the packet as the collector has been created 776 sendStanza(packet); 777 } 778 catch (InterruptedException | NotConnectedException | RuntimeException e) { 779 packetCollector.cancel(); 780 throw e; 781 } 782 return packetCollector; 783 } 784 785 @Override 786 public StanzaCollector createStanzaCollector(StanzaFilter packetFilter) { 787 StanzaCollector.Configuration configuration = StanzaCollector.newConfiguration().setStanzaFilter(packetFilter); 788 return createStanzaCollector(configuration); 789 } 790 791 @Override 792 public StanzaCollector createStanzaCollector(StanzaCollector.Configuration configuration) { 793 StanzaCollector collector = new StanzaCollector(this, configuration); 794 // Add the collector to the list of active collectors. 795 collectors.add(collector); 796 return collector; 797 } 798 799 @Override 800 public void removeStanzaCollector(StanzaCollector collector) { 801 collectors.remove(collector); 802 } 803 804 @Override 805 @Deprecated 806 public void addPacketListener(StanzaListener packetListener, StanzaFilter packetFilter) { 807 addAsyncStanzaListener(packetListener, packetFilter); 808 } 809 810 @Override 811 @Deprecated 812 public boolean removePacketListener(StanzaListener packetListener) { 813 return removeAsyncStanzaListener(packetListener); 814 } 815 816 @Override 817 public void addSyncStanzaListener(StanzaListener packetListener, StanzaFilter packetFilter) { 818 if (packetListener == null) { 819 throw new NullPointerException("Packet listener is null."); 820 } 821 ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter); 822 synchronized (syncRecvListeners) { 823 syncRecvListeners.put(packetListener, wrapper); 824 } 825 } 826 827 @Override 828 public boolean removeSyncStanzaListener(StanzaListener packetListener) { 829 synchronized (syncRecvListeners) { 830 return syncRecvListeners.remove(packetListener) != null; 831 } 832 } 833 834 @Override 835 public void addAsyncStanzaListener(StanzaListener packetListener, StanzaFilter packetFilter) { 836 if (packetListener == null) { 837 throw new NullPointerException("Packet listener is null."); 838 } 839 ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter); 840 synchronized (asyncRecvListeners) { 841 asyncRecvListeners.put(packetListener, wrapper); 842 } 843 } 844 845 @Override 846 public boolean removeAsyncStanzaListener(StanzaListener packetListener) { 847 synchronized (asyncRecvListeners) { 848 return asyncRecvListeners.remove(packetListener) != null; 849 } 850 } 851 852 @Deprecated 853 @Override 854 public void addPacketSendingListener(StanzaListener packetListener, StanzaFilter packetFilter) { 855 addStanzaSendingListener(packetListener, packetFilter); 856 } 857 858 @Override 859 public void addStanzaSendingListener(StanzaListener packetListener, StanzaFilter packetFilter) { 860 if (packetListener == null) { 861 throw new NullPointerException("Packet listener is null."); 862 } 863 ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter); 864 synchronized (sendListeners) { 865 sendListeners.put(packetListener, wrapper); 866 } 867 } 868 869 @Deprecated 870 @Override 871 public void removePacketSendingListener(StanzaListener packetListener) { 872 removeStanzaSendingListener(packetListener); 873 } 874 875 @Override 876 public void removeStanzaSendingListener(StanzaListener packetListener) { 877 synchronized (sendListeners) { 878 sendListeners.remove(packetListener); 879 } 880 } 881 882 /** 883 * Process all stanza(/packet) listeners for sending packets. 884 * <p> 885 * Compared to {@link #firePacketInterceptors(Stanza)}, the listeners will be invoked in a new thread. 886 * </p> 887 * 888 * @param packet the stanza(/packet) to process. 889 */ 890 @SuppressWarnings("javadoc") 891 protected void firePacketSendingListeners(final Stanza packet) { 892 final List<StanzaListener> listenersToNotify = new LinkedList<StanzaListener>(); 893 synchronized (sendListeners) { 894 for (ListenerWrapper listenerWrapper : sendListeners.values()) { 895 if (listenerWrapper.filterMatches(packet)) { 896 listenersToNotify.add(listenerWrapper.getListener()); 897 } 898 } 899 } 900 if (listenersToNotify.isEmpty()) { 901 return; 902 } 903 // Notify in a new thread, because we can 904 asyncGo(new Runnable() { 905 @Override 906 public void run() { 907 for (StanzaListener listener : listenersToNotify) { 908 try { 909 listener.processStanza(packet); 910 } 911 catch (Exception e) { 912 LOGGER.log(Level.WARNING, "Sending listener threw exception", e); 913 continue; 914 } 915 } 916 } 917 }); 918 } 919 920 @Deprecated 921 @Override 922 public void addPacketInterceptor(StanzaListener packetInterceptor, 923 StanzaFilter packetFilter) { 924 addStanzaInterceptor(packetInterceptor, packetFilter); 925 } 926 927 @Override 928 public void addStanzaInterceptor(StanzaListener packetInterceptor, 929 StanzaFilter packetFilter) { 930 if (packetInterceptor == null) { 931 throw new NullPointerException("Packet interceptor is null."); 932 } 933 InterceptorWrapper interceptorWrapper = new InterceptorWrapper(packetInterceptor, packetFilter); 934 synchronized (interceptors) { 935 interceptors.put(packetInterceptor, interceptorWrapper); 936 } 937 } 938 939 @Deprecated 940 @Override 941 public void removePacketInterceptor(StanzaListener packetInterceptor) { 942 removeStanzaInterceptor(packetInterceptor); 943 } 944 945 @Override 946 public void removeStanzaInterceptor(StanzaListener packetInterceptor) { 947 synchronized (interceptors) { 948 interceptors.remove(packetInterceptor); 949 } 950 } 951 952 /** 953 * Process interceptors. Interceptors may modify the stanza(/packet) that is about to be sent. 954 * Since the thread that requested to send the stanza(/packet) will invoke all interceptors, it 955 * is important that interceptors perform their work as soon as possible so that the 956 * thread does not remain blocked for a long period. 957 * 958 * @param packet the stanza(/packet) that is going to be sent to the server 959 */ 960 private void firePacketInterceptors(Stanza packet) { 961 List<StanzaListener> interceptorsToInvoke = new LinkedList<StanzaListener>(); 962 synchronized (interceptors) { 963 for (InterceptorWrapper interceptorWrapper : interceptors.values()) { 964 if (interceptorWrapper.filterMatches(packet)) { 965 interceptorsToInvoke.add(interceptorWrapper.getInterceptor()); 966 } 967 } 968 } 969 for (StanzaListener interceptor : interceptorsToInvoke) { 970 try { 971 interceptor.processStanza(packet); 972 } catch (Exception e) { 973 LOGGER.log(Level.SEVERE, "Packet interceptor threw exception", e); 974 } 975 } 976 } 977 978 /** 979 * Initialize the {@link #debugger}. You can specify a customized {@link SmackDebugger} 980 * by setup the system property <code>smack.debuggerClass</code> to the implementation. 981 * 982 * @throws IllegalStateException if the reader or writer isn't yet initialized. 983 * @throws IllegalArgumentException if the SmackDebugger can't be loaded. 984 */ 985 protected void initDebugger() { 986 if (reader == null || writer == null) { 987 throw new NullPointerException("Reader or writer isn't initialized."); 988 } 989 // If debugging is enabled, we open a window and write out all network traffic. 990 if (config.isDebuggerEnabled()) { 991 if (debugger == null) { 992 debugger = SmackConfiguration.createDebugger(this, writer, reader); 993 } 994 995 if (debugger == null) { 996 LOGGER.severe("Debugging enabled but could not find debugger class"); 997 } else { 998 // Obtain new reader and writer from the existing debugger 999 reader = debugger.newConnectionReader(reader); 1000 writer = debugger.newConnectionWriter(writer); 1001 } 1002 } 1003 } 1004 1005 @SuppressWarnings("deprecation") 1006 @Override 1007 public long getPacketReplyTimeout() { 1008 return getReplyTimeout(); 1009 } 1010 1011 @SuppressWarnings("deprecation") 1012 @Override 1013 public void setPacketReplyTimeout(long timeout) { 1014 setReplyTimeout(timeout); 1015 } 1016 1017 @Override 1018 public long getReplyTimeout() { 1019 return replyTimeout; 1020 } 1021 1022 @Override 1023 public void setReplyTimeout(long timeout) { 1024 replyTimeout = timeout; 1025 } 1026 1027 /** 1028 * Set the default value used to determine if new connection will reply to unknown IQ requests. The pre-configured 1029 * default is 'true'. 1030 * 1031 * @param replyToUnkownIqDefault 1032 * @see #setReplyToUnknownIq(boolean) 1033 * @deprecated Use {@link SmackConfiguration#setUnknownIqRequestReplyMode(org.jivesoftware.smack.SmackConfiguration.UnknownIqRequestReplyMode)} instead. 1034 */ 1035 @Deprecated 1036 // TODO Remove in Smack 4.3 1037 public static void setReplyToUnknownIqDefault(boolean replyToUnkownIqDefault) { 1038 SmackConfiguration.UnknownIqRequestReplyMode mode; 1039 if (replyToUnkownIqDefault) { 1040 mode = SmackConfiguration.UnknownIqRequestReplyMode.replyServiceUnavailable; 1041 } else { 1042 mode = SmackConfiguration.UnknownIqRequestReplyMode.doNotReply; 1043 } 1044 SmackConfiguration.setUnknownIqRequestReplyMode(mode); 1045 } 1046 1047 private SmackConfiguration.UnknownIqRequestReplyMode unknownIqRequestReplyMode = SmackConfiguration.getUnknownIqRequestReplyMode(); 1048 1049 /** 1050 * Set how Smack behaves when an unknown IQ request has been received. 1051 * 1052 * @param unknownIqRequestReplyMode reply mode. 1053 */ 1054 public void setUnknownIqRequestReplyMode(UnknownIqRequestReplyMode unknownIqRequestReplyMode) { 1055 this.unknownIqRequestReplyMode = Objects.requireNonNull(unknownIqRequestReplyMode, "Mode must not be null"); 1056 } 1057 1058 /** 1059 * Set if Smack will automatically send 1060 * {@link org.jivesoftware.smack.packet.XMPPError.Condition#feature_not_implemented} when a request IQ without a 1061 * registered {@link IQRequestHandler} is received. 1062 * 1063 * @param replyToUnknownIq whether Smack should reply to unknown IQs or not. 1064 * @deprecated use {@link AbstractXMPPConnection#setUnknownIqRequestReplyMode(UnknownIqRequestReplyMode)} instead. 1065 */ 1066 @Deprecated 1067 // TODO Remove in Smack 4.3 1068 public void setReplyToUnknownIq(boolean replyToUnknownIq) { 1069 SmackConfiguration.UnknownIqRequestReplyMode mode; 1070 if (replyToUnknownIq) { 1071 mode = SmackConfiguration.UnknownIqRequestReplyMode.replyServiceUnavailable; 1072 } else { 1073 mode = SmackConfiguration.UnknownIqRequestReplyMode.doNotReply; 1074 } 1075 unknownIqRequestReplyMode = mode; 1076 } 1077 1078 protected void parseAndProcessStanza(XmlPullParser parser) throws Exception { 1079 ParserUtils.assertAtStartTag(parser); 1080 int parserDepth = parser.getDepth(); 1081 Stanza stanza = null; 1082 try { 1083 stanza = PacketParserUtils.parseStanza(parser); 1084 } 1085 catch (Exception e) { 1086 CharSequence content = PacketParserUtils.parseContentDepth(parser, 1087 parserDepth); 1088 UnparseableStanza message = new UnparseableStanza(content, e); 1089 ParsingExceptionCallback callback = getParsingExceptionCallback(); 1090 if (callback != null) { 1091 callback.handleUnparsableStanza(message); 1092 } 1093 } 1094 ParserUtils.assertAtEndTag(parser); 1095 if (stanza != null) { 1096 processStanza(stanza); 1097 } 1098 } 1099 1100 /** 1101 * Processes a stanza(/packet) after it's been fully parsed by looping through the installed 1102 * stanza(/packet) collectors and listeners and letting them examine the stanza(/packet) to see if 1103 * they are a match with the filter. 1104 * 1105 * @param stanza the stanza to process. 1106 * @throws InterruptedException 1107 */ 1108 protected void processStanza(final Stanza stanza) throws InterruptedException { 1109 assert (stanza != null); 1110 lastStanzaReceived = System.currentTimeMillis(); 1111 // Deliver the incoming packet to listeners. 1112 invokeStanzaCollectorsAndNotifyRecvListeners(stanza); 1113 } 1114 1115 /** 1116 * Invoke {@link StanzaCollector#processStanza(Stanza)} for every 1117 * StanzaCollector with the given packet. Also notify the receive listeners with a matching stanza(/packet) filter about the packet. 1118 * <p> 1119 * This method will be invoked by the connections incoming processing thread which may be shared across multiple connections and 1120 * thus it is important that no user code, e.g. in form of a callback, is invoked by this method. For the same reason, 1121 * this method must not block for an extended period of time. 1122 * </p> 1123 * 1124 * @param packet the stanza(/packet) to notify the StanzaCollectors and receive listeners about. 1125 */ 1126 protected void invokeStanzaCollectorsAndNotifyRecvListeners(final Stanza packet) { 1127 if (packet instanceof IQ) { 1128 final IQ iq = (IQ) packet; 1129 final IQ.Type type = iq.getType(); 1130 switch (type) { 1131 case set: 1132 case get: 1133 final String key = XmppStringUtils.generateKey(iq.getChildElementName(), iq.getChildElementNamespace()); 1134 IQRequestHandler iqRequestHandler = null; 1135 switch (type) { 1136 case set: 1137 synchronized (setIqRequestHandler) { 1138 iqRequestHandler = setIqRequestHandler.get(key); 1139 } 1140 break; 1141 case get: 1142 synchronized (getIqRequestHandler) { 1143 iqRequestHandler = getIqRequestHandler.get(key); 1144 } 1145 break; 1146 default: 1147 throw new IllegalStateException("Should only encounter IQ type 'get' or 'set'"); 1148 } 1149 if (iqRequestHandler == null) { 1150 XMPPError.Condition replyCondition; 1151 switch (unknownIqRequestReplyMode) { 1152 case doNotReply: 1153 return; 1154 case replyFeatureNotImplemented: 1155 replyCondition = XMPPError.Condition.feature_not_implemented; 1156 break; 1157 case replyServiceUnavailable: 1158 replyCondition = XMPPError.Condition.service_unavailable; 1159 break; 1160 default: 1161 throw new AssertionError(); 1162 } 1163 1164 // If the IQ stanza is of type "get" or "set" with no registered IQ request handler, then answer an 1165 // IQ of type 'error' with condition 'service-unavailable'. 1166 ErrorIQ errorIQ = IQ.createErrorResponse(iq, XMPPError.getBuilder(( 1167 replyCondition))); 1168 try { 1169 sendStanza(errorIQ); 1170 } 1171 catch (InterruptedException | NotConnectedException e) { 1172 LOGGER.log(Level.WARNING, "Exception while sending error IQ to unkown IQ request", e); 1173 } 1174 } else { 1175 ExecutorService executorService = null; 1176 switch (iqRequestHandler.getMode()) { 1177 case sync: 1178 executorService = singleThreadedExecutorService; 1179 break; 1180 case async: 1181 executorService = CACHED_EXECUTOR_SERVICE; 1182 break; 1183 } 1184 final IQRequestHandler finalIqRequestHandler = iqRequestHandler; 1185 executorService.execute(new Runnable() { 1186 @Override 1187 public void run() { 1188 IQ response = finalIqRequestHandler.handleIQRequest(iq); 1189 if (response == null) { 1190 // It is not ideal if the IQ request handler does not return an IQ response, because RFC 1191 // 6120 § 8.1.2 does specify that a response is mandatory. But some APIs, mostly the 1192 // file transfer one, does not always return a result, so we need to handle this case. 1193 // Also sometimes a request handler may decide that it's better to not send a response, 1194 // e.g. to avoid presence leaks. 1195 return; 1196 } 1197 try { 1198 sendStanza(response); 1199 } 1200 catch (InterruptedException | NotConnectedException e) { 1201 LOGGER.log(Level.WARNING, "Exception while sending response to IQ request", e); 1202 } 1203 } 1204 }); 1205 // The following returns makes it impossible for packet listeners and collectors to 1206 // filter for IQ request stanzas, i.e. IQs of type 'set' or 'get'. This is the 1207 // desired behavior. 1208 return; 1209 } 1210 break; 1211 default: 1212 break; 1213 } 1214 } 1215 1216 // First handle the async recv listeners. Note that this code is very similar to what follows a few lines below, 1217 // the only difference is that asyncRecvListeners is used here and that the packet listeners are started in 1218 // their own thread. 1219 final Collection<StanzaListener> listenersToNotify = new LinkedList<StanzaListener>(); 1220 synchronized (asyncRecvListeners) { 1221 for (ListenerWrapper listenerWrapper : asyncRecvListeners.values()) { 1222 if (listenerWrapper.filterMatches(packet)) { 1223 listenersToNotify.add(listenerWrapper.getListener()); 1224 } 1225 } 1226 } 1227 1228 for (final StanzaListener listener : listenersToNotify) { 1229 asyncGo(new Runnable() { 1230 @Override 1231 public void run() { 1232 try { 1233 listener.processStanza(packet); 1234 } catch (Exception e) { 1235 LOGGER.log(Level.SEVERE, "Exception in async packet listener", e); 1236 } 1237 } 1238 }); 1239 } 1240 1241 // Loop through all collectors and notify the appropriate ones. 1242 for (StanzaCollector collector : collectors) { 1243 collector.processStanza(packet); 1244 } 1245 1246 // Notify the receive listeners interested in the packet 1247 listenersToNotify.clear(); 1248 synchronized (syncRecvListeners) { 1249 for (ListenerWrapper listenerWrapper : syncRecvListeners.values()) { 1250 if (listenerWrapper.filterMatches(packet)) { 1251 listenersToNotify.add(listenerWrapper.getListener()); 1252 } 1253 } 1254 } 1255 1256 // Decouple incoming stanza processing from listener invocation. Unlike async listeners, this uses a single 1257 // threaded executor service and therefore keeps the order. 1258 singleThreadedExecutorService.execute(new Runnable() { 1259 @Override 1260 public void run() { 1261 for (StanzaListener listener : listenersToNotify) { 1262 try { 1263 listener.processStanza(packet); 1264 } catch (NotConnectedException e) { 1265 LOGGER.log(Level.WARNING, "Got not connected exception, aborting", e); 1266 break; 1267 } catch (Exception e) { 1268 LOGGER.log(Level.SEVERE, "Exception in packet listener", e); 1269 } 1270 } 1271 } 1272 }); 1273 1274 } 1275 1276 /** 1277 * Sets whether the connection has already logged in the server. This method assures that the 1278 * {@link #wasAuthenticated} flag is never reset once it has ever been set. 1279 * 1280 */ 1281 protected void setWasAuthenticated() { 1282 // Never reset the flag if the connection has ever been authenticated 1283 if (!wasAuthenticated) { 1284 wasAuthenticated = authenticated; 1285 } 1286 } 1287 1288 protected void callConnectionConnectedListener() { 1289 for (ConnectionListener listener : connectionListeners) { 1290 listener.connected(this); 1291 } 1292 } 1293 1294 protected void callConnectionAuthenticatedListener(boolean resumed) { 1295 for (ConnectionListener listener : connectionListeners) { 1296 try { 1297 listener.authenticated(this, resumed); 1298 } catch (Exception e) { 1299 // Catch and print any exception so we can recover 1300 // from a faulty listener and finish the shutdown process 1301 LOGGER.log(Level.SEVERE, "Exception in authenticated listener", e); 1302 } 1303 } 1304 } 1305 1306 void callConnectionClosedListener() { 1307 for (ConnectionListener listener : connectionListeners) { 1308 try { 1309 listener.connectionClosed(); 1310 } 1311 catch (Exception e) { 1312 // Catch and print any exception so we can recover 1313 // from a faulty listener and finish the shutdown process 1314 LOGGER.log(Level.SEVERE, "Error in listener while closing connection", e); 1315 } 1316 } 1317 } 1318 1319 protected void callConnectionClosedOnErrorListener(Exception e) { 1320 boolean logWarning = true; 1321 if (e instanceof StreamErrorException) { 1322 StreamErrorException see = (StreamErrorException) e; 1323 if (see.getStreamError().getCondition() == StreamError.Condition.not_authorized 1324 && wasAuthenticated) { 1325 logWarning = false; 1326 LOGGER.log(Level.FINE, 1327 "Connection closed with not-authorized stream error after it was already authenticated. The account was likely deleted/unregistered on the server"); 1328 } 1329 } 1330 if (logWarning) { 1331 LOGGER.log(Level.WARNING, "Connection " + this + " closed with error", e); 1332 } 1333 for (ConnectionListener listener : connectionListeners) { 1334 try { 1335 listener.connectionClosedOnError(e); 1336 } 1337 catch (Exception e2) { 1338 // Catch and print any exception so we can recover 1339 // from a faulty listener 1340 LOGGER.log(Level.SEVERE, "Error in listener while closing connection", e2); 1341 } 1342 } 1343 } 1344 1345 /** 1346 * Sends a notification indicating that the connection was reconnected successfully. 1347 */ 1348 // TODO: Remove in Smack 4.3 1349 @Deprecated 1350 protected void notifyReconnection() { 1351 // Notify connection listeners of the reconnection. 1352 for (ConnectionListener listener : connectionListeners) { 1353 try { 1354 listener.reconnectionSuccessful(); 1355 } 1356 catch (Exception e) { 1357 // Catch and print any exception so we can recover 1358 // from a faulty listener 1359 LOGGER.log(Level.WARNING, "notifyReconnection()", e); 1360 } 1361 } 1362 } 1363 1364 /** 1365 * A wrapper class to associate a stanza(/packet) filter with a listener. 1366 */ 1367 protected static class ListenerWrapper { 1368 1369 private final StanzaListener packetListener; 1370 private final StanzaFilter packetFilter; 1371 1372 /** 1373 * Create a class which associates a stanza(/packet) filter with a listener. 1374 * 1375 * @param packetListener the stanza(/packet) listener. 1376 * @param packetFilter the associated filter or null if it listen for all packets. 1377 */ 1378 public ListenerWrapper(StanzaListener packetListener, StanzaFilter packetFilter) { 1379 this.packetListener = packetListener; 1380 this.packetFilter = packetFilter; 1381 } 1382 1383 public boolean filterMatches(Stanza packet) { 1384 return packetFilter == null || packetFilter.accept(packet); 1385 } 1386 1387 public StanzaListener getListener() { 1388 return packetListener; 1389 } 1390 } 1391 1392 /** 1393 * A wrapper class to associate a stanza(/packet) filter with an interceptor. 1394 */ 1395 protected static class InterceptorWrapper { 1396 1397 private final StanzaListener packetInterceptor; 1398 private final StanzaFilter packetFilter; 1399 1400 /** 1401 * Create a class which associates a stanza(/packet) filter with an interceptor. 1402 * 1403 * @param packetInterceptor the interceptor. 1404 * @param packetFilter the associated filter or null if it intercepts all packets. 1405 */ 1406 public InterceptorWrapper(StanzaListener packetInterceptor, StanzaFilter packetFilter) { 1407 this.packetInterceptor = packetInterceptor; 1408 this.packetFilter = packetFilter; 1409 } 1410 1411 public boolean filterMatches(Stanza packet) { 1412 return packetFilter == null || packetFilter.accept(packet); 1413 } 1414 1415 public StanzaListener getInterceptor() { 1416 return packetInterceptor; 1417 } 1418 } 1419 1420 @Override 1421 public int getConnectionCounter() { 1422 return connectionCounterValue; 1423 } 1424 1425 @Override 1426 public void setFromMode(FromMode fromMode) { 1427 this.fromMode = fromMode; 1428 } 1429 1430 @Override 1431 public FromMode getFromMode() { 1432 return this.fromMode; 1433 } 1434 1435 @Override 1436 protected void finalize() throws Throwable { 1437 LOGGER.fine("finalizing " + this + ": Shutting down executor services"); 1438 try { 1439 // It's usually not a good idea to rely on finalize. But this is the easiest way to 1440 // avoid the "Smack Listener Processor" leaking. The thread(s) of the executor have a 1441 // reference to their ExecutorService which prevents the ExecutorService from being 1442 // gc'ed. It is possible that the XMPPConnection instance is gc'ed while the 1443 // listenerExecutor ExecutorService call not be gc'ed until it got shut down. 1444 singleThreadedExecutorService.shutdownNow(); 1445 } catch (Throwable t) { 1446 LOGGER.log(Level.WARNING, "finalize() threw trhowable", t); 1447 } 1448 finally { 1449 super.finalize(); 1450 } 1451 } 1452 1453 protected final void parseFeatures(XmlPullParser parser) throws Exception { 1454 streamFeatures.clear(); 1455 final int initialDepth = parser.getDepth(); 1456 while (true) { 1457 int eventType = parser.next(); 1458 1459 if (eventType == XmlPullParser.START_TAG && parser.getDepth() == initialDepth + 1) { 1460 ExtensionElement streamFeature = null; 1461 String name = parser.getName(); 1462 String namespace = parser.getNamespace(); 1463 switch (name) { 1464 case StartTls.ELEMENT: 1465 streamFeature = PacketParserUtils.parseStartTlsFeature(parser); 1466 break; 1467 case Mechanisms.ELEMENT: 1468 streamFeature = new Mechanisms(PacketParserUtils.parseMechanisms(parser)); 1469 break; 1470 case Bind.ELEMENT: 1471 streamFeature = Bind.Feature.INSTANCE; 1472 break; 1473 case Session.ELEMENT: 1474 streamFeature = PacketParserUtils.parseSessionFeature(parser); 1475 break; 1476 case Compress.Feature.ELEMENT: 1477 streamFeature = PacketParserUtils.parseCompressionFeature(parser); 1478 break; 1479 default: 1480 ExtensionElementProvider<ExtensionElement> provider = ProviderManager.getStreamFeatureProvider(name, namespace); 1481 if (provider != null) { 1482 streamFeature = provider.parse(parser); 1483 } 1484 break; 1485 } 1486 if (streamFeature != null) { 1487 addStreamFeature(streamFeature); 1488 } 1489 } 1490 else if (eventType == XmlPullParser.END_TAG && parser.getDepth() == initialDepth) { 1491 break; 1492 } 1493 } 1494 1495 if (hasFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE)) { 1496 // Only proceed with SASL auth if TLS is disabled or if the server doesn't announce it 1497 if (!hasFeature(StartTls.ELEMENT, StartTls.NAMESPACE) 1498 || config.getSecurityMode() == SecurityMode.disabled) { 1499 tlsHandled.reportSuccess(); 1500 saslFeatureReceived.reportSuccess(); 1501 } 1502 } 1503 1504 // If the server reported the bind feature then we are that that we did SASL and maybe 1505 // STARTTLS. We can then report that the last 'stream:features' have been parsed 1506 if (hasFeature(Bind.ELEMENT, Bind.NAMESPACE)) { 1507 if (!hasFeature(Compress.Feature.ELEMENT, Compress.NAMESPACE) 1508 || !config.isCompressionEnabled()) { 1509 // This was was last features from the server is either it did not contain 1510 // compression or if we disabled it 1511 lastFeaturesReceived.reportSuccess(); 1512 } 1513 } 1514 afterFeaturesReceived(); 1515 } 1516 1517 @SuppressWarnings("unused") 1518 protected void afterFeaturesReceived() throws SecurityRequiredException, NotConnectedException, InterruptedException { 1519 // Default implementation does nothing 1520 } 1521 1522 @SuppressWarnings("unchecked") 1523 @Override 1524 public <F extends ExtensionElement> F getFeature(String element, String namespace) { 1525 return (F) streamFeatures.get(XmppStringUtils.generateKey(element, namespace)); 1526 } 1527 1528 @Override 1529 public boolean hasFeature(String element, String namespace) { 1530 return getFeature(element, namespace) != null; 1531 } 1532 1533 protected void addStreamFeature(ExtensionElement feature) { 1534 String key = XmppStringUtils.generateKey(feature.getElementName(), feature.getNamespace()); 1535 streamFeatures.put(key, feature); 1536 } 1537 1538 @Override 1539 public void sendStanzaWithResponseCallback(Stanza stanza, StanzaFilter replyFilter, 1540 StanzaListener callback) throws NotConnectedException, InterruptedException { 1541 sendStanzaWithResponseCallback(stanza, replyFilter, callback, null); 1542 } 1543 1544 @Override 1545 public void sendStanzaWithResponseCallback(Stanza stanza, StanzaFilter replyFilter, 1546 StanzaListener callback, ExceptionCallback exceptionCallback) 1547 throws NotConnectedException, InterruptedException { 1548 sendStanzaWithResponseCallback(stanza, replyFilter, callback, exceptionCallback, 1549 getReplyTimeout()); 1550 } 1551 1552 @Override 1553 public void sendStanzaWithResponseCallback(Stanza stanza, final StanzaFilter replyFilter, 1554 final StanzaListener callback, final ExceptionCallback exceptionCallback, 1555 long timeout) throws NotConnectedException, InterruptedException { 1556 Objects.requireNonNull(stanza, "stanza must not be null"); 1557 // While Smack allows to add PacketListeners with a PacketFilter value of 'null', we 1558 // disallow it here in the async API as it makes no sense 1559 Objects.requireNonNull(replyFilter, "replyFilter must not be null"); 1560 Objects.requireNonNull(callback, "callback must not be null"); 1561 1562 final StanzaListener packetListener = new StanzaListener() { 1563 @Override 1564 public void processStanza(Stanza packet) throws NotConnectedException, InterruptedException, NotLoggedInException { 1565 boolean removed = removeAsyncStanzaListener(this); 1566 if (!removed) { 1567 // We lost a race against the "no response" handling runnable. Avoid calling the callback, as the 1568 // exception callback will be invoked (if any). 1569 return; 1570 } 1571 try { 1572 XMPPErrorException.ifHasErrorThenThrow(packet); 1573 callback.processStanza(packet); 1574 } 1575 catch (XMPPErrorException e) { 1576 if (exceptionCallback != null) { 1577 exceptionCallback.processException(e); 1578 } 1579 } 1580 } 1581 }; 1582 schedule(new Runnable() { 1583 @Override 1584 public void run() { 1585 boolean removed = removeAsyncStanzaListener(packetListener); 1586 // If the packetListener got removed, then it was never run and 1587 // we never received a response, inform the exception callback 1588 if (removed && exceptionCallback != null) { 1589 Exception exception; 1590 if (!isConnected()) { 1591 // If the connection is no longer connected, throw a not connected exception. 1592 exception = new NotConnectedException(AbstractXMPPConnection.this, replyFilter); 1593 } else { 1594 exception = NoResponseException.newWith(AbstractXMPPConnection.this, replyFilter); 1595 } 1596 final Exception exceptionToProcess = exception; 1597 Async.go(new Runnable() { 1598 @Override 1599 public void run() { 1600 exceptionCallback.processException(exceptionToProcess); 1601 } 1602 }); 1603 } 1604 } 1605 }, timeout, TimeUnit.MILLISECONDS); 1606 addAsyncStanzaListener(packetListener, replyFilter); 1607 sendStanza(stanza); 1608 } 1609 1610 @Override 1611 public void sendIqWithResponseCallback(IQ iqRequest, StanzaListener callback) 1612 throws NotConnectedException, InterruptedException { 1613 sendIqWithResponseCallback(iqRequest, callback, null); 1614 } 1615 1616 @Override 1617 public void sendIqWithResponseCallback(IQ iqRequest, StanzaListener callback, 1618 ExceptionCallback exceptionCallback) throws NotConnectedException, InterruptedException { 1619 sendIqWithResponseCallback(iqRequest, callback, exceptionCallback, getReplyTimeout()); 1620 } 1621 1622 @Override 1623 public void sendIqWithResponseCallback(IQ iqRequest, final StanzaListener callback, 1624 final ExceptionCallback exceptionCallback, long timeout) 1625 throws NotConnectedException, InterruptedException { 1626 StanzaFilter replyFilter = new IQReplyFilter(iqRequest, this); 1627 sendStanzaWithResponseCallback(iqRequest, replyFilter, callback, exceptionCallback, timeout); 1628 } 1629 1630 @Override 1631 public void addOneTimeSyncCallback(final StanzaListener callback, final StanzaFilter packetFilter) { 1632 final StanzaListener packetListener = new StanzaListener() { 1633 @Override 1634 public void processStanza(Stanza packet) throws NotConnectedException, InterruptedException, NotLoggedInException { 1635 try { 1636 callback.processStanza(packet); 1637 } finally { 1638 removeSyncStanzaListener(this); 1639 } 1640 } 1641 }; 1642 addSyncStanzaListener(packetListener, packetFilter); 1643 schedule(new Runnable() { 1644 @Override 1645 public void run() { 1646 removeSyncStanzaListener(packetListener); 1647 } 1648 }, getReplyTimeout(), TimeUnit.MILLISECONDS); 1649 } 1650 1651 @Override 1652 public IQRequestHandler registerIQRequestHandler(final IQRequestHandler iqRequestHandler) { 1653 final String key = XmppStringUtils.generateKey(iqRequestHandler.getElement(), iqRequestHandler.getNamespace()); 1654 switch (iqRequestHandler.getType()) { 1655 case set: 1656 synchronized (setIqRequestHandler) { 1657 return setIqRequestHandler.put(key, iqRequestHandler); 1658 } 1659 case get: 1660 synchronized (getIqRequestHandler) { 1661 return getIqRequestHandler.put(key, iqRequestHandler); 1662 } 1663 default: 1664 throw new IllegalArgumentException("Only IQ type of 'get' and 'set' allowed"); 1665 } 1666 } 1667 1668 @Override 1669 public final IQRequestHandler unregisterIQRequestHandler(IQRequestHandler iqRequestHandler) { 1670 return unregisterIQRequestHandler(iqRequestHandler.getElement(), iqRequestHandler.getNamespace(), 1671 iqRequestHandler.getType()); 1672 } 1673 1674 @Override 1675 public IQRequestHandler unregisterIQRequestHandler(String element, String namespace, IQ.Type type) { 1676 final String key = XmppStringUtils.generateKey(element, namespace); 1677 switch (type) { 1678 case set: 1679 synchronized (setIqRequestHandler) { 1680 return setIqRequestHandler.remove(key); 1681 } 1682 case get: 1683 synchronized (getIqRequestHandler) { 1684 return getIqRequestHandler.remove(key); 1685 } 1686 default: 1687 throw new IllegalArgumentException("Only IQ type of 'get' and 'set' allowed"); 1688 } 1689 } 1690 1691 private long lastStanzaReceived; 1692 1693 @Override 1694 public long getLastStanzaReceived() { 1695 return lastStanzaReceived; 1696 } 1697 1698 /** 1699 * Install a parsing exception callback, which will be invoked once an exception is encountered while parsing a 1700 * stanza. 1701 * 1702 * @param callback the callback to install 1703 */ 1704 public void setParsingExceptionCallback(ParsingExceptionCallback callback) { 1705 parsingExceptionCallback = callback; 1706 } 1707 1708 /** 1709 * Get the current active parsing exception callback. 1710 * 1711 * @return the active exception callback or null if there is none 1712 */ 1713 public ParsingExceptionCallback getParsingExceptionCallback() { 1714 return parsingExceptionCallback; 1715 } 1716 1717 @Override 1718 public final String toString() { 1719 EntityFullJid localEndpoint = getUser(); 1720 String localEndpointString = (localEndpoint == null ? "not-authenticated" : localEndpoint.toString()); 1721 return getClass().getSimpleName() + '[' + localEndpointString + "] (" + getConnectionCounter() + ')'; 1722 } 1723 1724 protected final void asyncGo(Runnable runnable) { 1725 CACHED_EXECUTOR_SERVICE.execute(runnable); 1726 } 1727 1728 protected final ScheduledFuture<?> schedule(Runnable runnable, long delay, TimeUnit unit) { 1729 return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, unit); 1730 } 1731}