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