001/** 002 * 003 * Copyright 2018-2021 Florian Schmaus 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.c2s; 018 019import java.io.IOException; 020import java.security.cert.CertificateException; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.List; 027import java.util.ListIterator; 028import java.util.Map; 029import java.util.concurrent.CopyOnWriteArrayList; 030import java.util.concurrent.TimeUnit; 031import java.util.logging.Level; 032import java.util.logging.Logger; 033 034import javax.net.ssl.SSLSession; 035 036import org.jivesoftware.smack.AbstractXMPPConnection; 037import org.jivesoftware.smack.SmackException; 038import org.jivesoftware.smack.SmackException.NoResponseException; 039import org.jivesoftware.smack.SmackException.NotConnectedException; 040import org.jivesoftware.smack.SmackFuture; 041import org.jivesoftware.smack.XMPPException; 042import org.jivesoftware.smack.XMPPException.FailedNonzaException; 043import org.jivesoftware.smack.XMPPException.StreamErrorException; 044import org.jivesoftware.smack.XMPPException.XMPPErrorException; 045import org.jivesoftware.smack.XmppInputOutputFilter; 046import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsFailed; 047import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsResult; 048import org.jivesoftware.smack.c2s.XmppClientToServerTransport.LookupConnectionEndpointsSuccess; 049import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal; 050import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext; 051import org.jivesoftware.smack.fsm.ConnectionStateEvent; 052import org.jivesoftware.smack.fsm.ConnectionStateMachineListener; 053import org.jivesoftware.smack.fsm.LoginContext; 054import org.jivesoftware.smack.fsm.NoOpState; 055import org.jivesoftware.smack.fsm.State; 056import org.jivesoftware.smack.fsm.StateDescriptor; 057import org.jivesoftware.smack.fsm.StateDescriptorGraph; 058import org.jivesoftware.smack.fsm.StateDescriptorGraph.GraphVertex; 059import org.jivesoftware.smack.fsm.StateMachineException; 060import org.jivesoftware.smack.fsm.StateTransitionResult; 061import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult; 062import org.jivesoftware.smack.internal.AbstractStats; 063import org.jivesoftware.smack.internal.SmackTlsContext; 064import org.jivesoftware.smack.packet.AbstractStreamClose; 065import org.jivesoftware.smack.packet.AbstractStreamOpen; 066import org.jivesoftware.smack.packet.IQ; 067import org.jivesoftware.smack.packet.Message; 068import org.jivesoftware.smack.packet.Nonza; 069import org.jivesoftware.smack.packet.Presence; 070import org.jivesoftware.smack.packet.Stanza; 071import org.jivesoftware.smack.packet.StreamError; 072import org.jivesoftware.smack.packet.TopLevelStreamElement; 073import org.jivesoftware.smack.packet.XmlEnvironment; 074import org.jivesoftware.smack.parsing.SmackParsingException; 075import org.jivesoftware.smack.sasl.SASLErrorException; 076import org.jivesoftware.smack.sasl.SASLMechanism; 077import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; 078import org.jivesoftware.smack.util.ExtendedAppendable; 079import org.jivesoftware.smack.util.PacketParserUtils; 080import org.jivesoftware.smack.util.StringUtils; 081import org.jivesoftware.smack.util.Supplier; 082import org.jivesoftware.smack.xml.XmlPullParser; 083import org.jivesoftware.smack.xml.XmlPullParserException; 084 085import org.jxmpp.jid.DomainBareJid; 086import org.jxmpp.jid.parts.Resourcepart; 087import org.jxmpp.util.XmppStringUtils; 088 089public final class ModularXmppClientToServerConnection extends AbstractXMPPConnection { 090 091 private static final Logger LOGGER = Logger.getLogger(ModularXmppClientToServerConnectionConfiguration.class.getName()); 092 093 private final ArrayBlockingQueueWithShutdown<TopLevelStreamElement> outgoingElementsQueue = new ArrayBlockingQueueWithShutdown<>( 094 100, true); 095 096 private XmppClientToServerTransport activeTransport; 097 098 private final List<ConnectionStateMachineListener> connectionStateMachineListeners = new CopyOnWriteArrayList<>(); 099 100 private boolean featuresReceived; 101 102 private boolean streamResumed; 103 104 private GraphVertex<State> currentStateVertex; 105 106 private List<State> walkFromDisconnectToAuthenticated; 107 108 private final ModularXmppClientToServerConnectionConfiguration configuration; 109 110 private final ModularXmppClientToServerConnectionInternal connectionInternal; 111 112 private final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor>> connectionModules = new HashMap<>(); 113 114 private final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport> transports = new HashMap<>(); 115 /** 116 * This is one of those cases where the field is modified by one thread and read by another. We currently use 117 * CopyOnWriteArrayList but should potentially use a VarHandle once Smack supports them. 118 */ 119 private final List<XmppInputOutputFilter> inputOutputFilters = new CopyOnWriteArrayList<>(); 120 121 private List<XmppInputOutputFilter> previousInputOutputFilters; 122 123 public ModularXmppClientToServerConnection(ModularXmppClientToServerConnectionConfiguration configuration) { 124 super(configuration); 125 126 this.configuration = configuration; 127 128 // Construct the internal connection API. 129 connectionInternal = new ModularXmppClientToServerConnectionInternal(this, getReactor(), debugger, outgoingElementsQueue) { 130 131 @Override 132 public void parseAndProcessElement(String wrappedCompleteElement) { 133 ModularXmppClientToServerConnection.this.parseAndProcessElement(wrappedCompleteElement); 134 } 135 136 @Override 137 public void notifyConnectionError(Exception e) { 138 ModularXmppClientToServerConnection.this.notifyConnectionError(e); 139 } 140 141 @Override 142 public String onStreamOpen(XmlPullParser parser) { 143 return ModularXmppClientToServerConnection.this.onStreamOpen(parser); 144 } 145 146 @Override 147 public void onStreamClosed() { 148 ModularXmppClientToServerConnection.this.closingStreamReceived = true; 149 notifyWaitingThreads(); 150 } 151 152 @Override 153 public void fireFirstLevelElementSendListeners(TopLevelStreamElement element) { 154 ModularXmppClientToServerConnection.this.firePacketSendingListeners(element); 155 } 156 157 @Override 158 public void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { 159 ModularXmppClientToServerConnection.this.invokeConnectionStateMachineListener(connectionStateEvent); 160 } 161 162 @Override 163 public XmlEnvironment getOutgoingStreamXmlEnvironment() { 164 return outgoingStreamXmlEnvironment; 165 } 166 167 @Override 168 public void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) { 169 inputOutputFilters.add(0, xmppInputOutputFilter); 170 } 171 172 @Override 173 public ListIterator<XmppInputOutputFilter> getXmppInputOutputFilterBeginIterator() { 174 return inputOutputFilters.listIterator(); 175 } 176 177 @Override 178 public ListIterator<XmppInputOutputFilter> getXmppInputOutputFilterEndIterator() { 179 return inputOutputFilters.listIterator(inputOutputFilters.size()); 180 } 181 182 @Override 183 public void waitForFeaturesReceived(String waitFor) throws InterruptedException, SmackException, XMPPException { 184 ModularXmppClientToServerConnection.this.waitForFeaturesReceived(waitFor); 185 } 186 187 @Override 188 public void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, 189 SmackException, XMPPException { 190 ModularXmppClientToServerConnection.this.newStreamOpenWaitForFeaturesSequence(waitFor); 191 } 192 193 @Override 194 public SmackTlsContext getSmackTlsContext() { 195 return ModularXmppClientToServerConnection.this.getSmackTlsContext(); 196 } 197 198 @Override 199 public <SN extends Nonza, FN extends Nonza> SN sendAndWaitForResponse(Nonza nonza, Class<SN> successNonzaClass, 200 Class<FN> failedNonzaClass) throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException { 201 return ModularXmppClientToServerConnection.this.sendAndWaitForResponse(nonza, successNonzaClass, failedNonzaClass); 202 } 203 204 @Override 205 public void asyncGo(Runnable runnable) { 206 AbstractXMPPConnection.asyncGo(runnable); 207 } 208 209 @Override 210 public void waitForConditionOrThrowConnectionException(Supplier<Boolean> condition, String waitFor) 211 throws InterruptedException, SmackException, XMPPException { 212 ModularXmppClientToServerConnection.this.waitForConditionOrThrowConnectionException(condition, waitFor); 213 } 214 215 @Override 216 public void notifyWaitingThreads() { 217 ModularXmppClientToServerConnection.this.notifyWaitingThreads(); 218 } 219 220 @Override 221 public void setCompressionEnabled(boolean compressionEnabled) { 222 ModularXmppClientToServerConnection.this.compressionEnabled = compressionEnabled; 223 } 224 225 @Override 226 public void setTransport(XmppClientToServerTransport xmppTransport) { 227 ModularXmppClientToServerConnection.this.activeTransport = xmppTransport; 228 ModularXmppClientToServerConnection.this.connected = true; 229 } 230 231 }; 232 233 // Construct the modules from the module descriptor. We do this before constructing the state graph, as the 234 // modules are sometimes used to construct the states. 235 for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : configuration.moduleDescriptors) { 236 Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> moduleDescriptorClass = moduleDescriptor.getClass(); 237 ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor> connectionModule = moduleDescriptor.constructXmppConnectionModule(connectionInternal); 238 connectionModules.put(moduleDescriptorClass, connectionModule); 239 240 XmppClientToServerTransport transport = connectionModule.getTransport(); 241 // Not every module may provide a transport. 242 if (transport != null) { 243 transports.put(moduleDescriptorClass, transport); 244 } 245 } 246 247 GraphVertex<StateDescriptor> initialStateDescriptorVertex = configuration.initialStateDescriptorVertex; 248 // Convert the graph of state descriptors to a graph of states, bound to this very connection. 249 currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, connectionInternal); 250 } 251 252 @SuppressWarnings("unchecked") 253 public <CM extends ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor>> CM getConnectionModuleFor( 254 Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> descriptorClass) { 255 return (CM) connectionModules.get(descriptorClass); 256 } 257 258 @Override 259 protected void loginInternal(String username, String password, Resourcepart resource) 260 throws XMPPException, SmackException, IOException, InterruptedException { 261 WalkStateGraphContext walkStateGraphContext = buildNewWalkTo( 262 AuthenticatedAndResourceBoundStateDescriptor.class).withLoginContext(username, password, 263 resource).build(); 264 walkStateGraph(walkStateGraphContext); 265 } 266 267 private WalkStateGraphContext.Builder buildNewWalkTo(Class<? extends StateDescriptor> finalStateClass) { 268 return WalkStateGraphContext.builder(currentStateVertex.getElement().getStateDescriptor().getClass(), finalStateClass); 269 } 270 271 /** 272 * Unwind the state. This will revert the effects of the state by calling {@link State#resetState()} prior issuing a 273 * connection state event of {@link ConnectionStateEvent#StateRevertBackwardsWalk}. 274 * 275 * @param revertedState the state which is going to get reverted. 276 */ 277 private void unwindState(State revertedState) { 278 invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState)); 279 revertedState.resetState(); 280 } 281 282 private void walkStateGraph(WalkStateGraphContext walkStateGraphContext) 283 throws XMPPException, IOException, SmackException, InterruptedException { 284 // Save a copy of the current state 285 GraphVertex<State> previousStateVertex = currentStateVertex; 286 try { 287 walkStateGraphInternal(walkStateGraphContext); 288 } catch (IOException | SmackException | InterruptedException | XMPPException e) { 289 currentStateVertex = previousStateVertex; 290 // Unwind the state. 291 State revertedState = currentStateVertex.getElement(); 292 unwindState(revertedState); 293 throw e; 294 } 295 } 296 297 private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) 298 throws IOException, SmackException, InterruptedException, XMPPException { 299 // Save a copy of the current state 300 final GraphVertex<State> initialStateVertex = currentStateVertex; 301 final State initialState = initialStateVertex.getElement(); 302 final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor(); 303 304 walkStateGraphContext.recordWalkTo(initialState); 305 306 // Check if this is the walk's final state. 307 if (walkStateGraphContext.isWalksFinalState(initialStateDescriptor)) { 308 // If this is used as final state, then it should be marked as such. 309 assert initialStateDescriptor.isFinalState(); 310 311 // We reached the final state. 312 invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState)); 313 return; 314 } 315 316 List<GraphVertex<State>> outgoingStateEdges = initialStateVertex.getOutgoingEdges(); 317 318 // See if we need to handle mandatory intermediate states. 319 GraphVertex<State> mandatoryIntermediateStateVertex = walkStateGraphContext.maybeReturnMandatoryImmediateState(outgoingStateEdges); 320 if (mandatoryIntermediateStateVertex != null) { 321 StateTransitionResult reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext); 322 323 if (reason instanceof StateTransitionResult.Success) { 324 walkStateGraph(walkStateGraphContext); 325 return; 326 } 327 328 // We could not enter a mandatory intermediate state. Throw here. 329 throw new StateMachineException.SmackMandatoryStateFailedException( 330 mandatoryIntermediateStateVertex.getElement(), reason); 331 } 332 333 for (Iterator<GraphVertex<State>> it = outgoingStateEdges.iterator(); it.hasNext();) { 334 GraphVertex<State> successorStateVertex = it.next(); 335 State successorState = successorStateVertex.getElement(); 336 337 // Ignore successorStateVertex if the only way to the final state is via the initial state. This happens 338 // typically if we are in the ConnectedButUnauthenticated state on the way to ResourceboundAndAuthenticated, 339 // where we do not want to walk via InstantShutdown/Shtudown in a cycle over the initial state towards this 340 // state. 341 if (walkStateGraphContext.wouldCauseCycle(successorStateVertex)) { 342 // Ignore this successor. 343 invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionIgnoredDueCycle(initialStateVertex, successorStateVertex)); 344 } else { 345 StateTransitionResult result = attemptEnterState(successorStateVertex, walkStateGraphContext); 346 347 if (result instanceof StateTransitionResult.Success) { 348 break; 349 } 350 351 // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then we 352 // just record this value and go on from there. Note that reason may be null, which is returned by 353 // attemptEnterState in case the state was already successfully handled. If this is the case, then we don't 354 // record it. 355 if (result != null) { 356 walkStateGraphContext.recordFailedState(successorState, result); 357 } 358 } 359 360 if (!it.hasNext()) { 361 throw StateMachineException.SmackStateGraphDeadEndException.from(walkStateGraphContext, 362 initialStateVertex); 363 } 364 } 365 366 // Walk the state graph by recursion. 367 walkStateGraph(walkStateGraphContext); 368 } 369 370 /** 371 * Attempt to enter a state. Note that this method may return <code>null</code> if this state can be safely ignored. 372 * 373 * @param successorStateVertex the successor state vertex. 374 * @param walkStateGraphContext the "walk state graph" context. 375 * @return A state transition result or <code>null</code> if this state can be ignored. 376 * @throws SmackException if Smack detected an exceptional situation. 377 * @throws XMPPException if an XMPP protocol error was received. 378 * @throws IOException if an I/O error occurred. 379 * @throws InterruptedException if the calling thread was interrupted. 380 */ 381 private StateTransitionResult attemptEnterState(GraphVertex<State> successorStateVertex, 382 WalkStateGraphContext walkStateGraphContext) throws SmackException, XMPPException, 383 IOException, InterruptedException { 384 final GraphVertex<State> initialStateVertex = currentStateVertex; 385 final State initialState = initialStateVertex.getElement(); 386 final State successorState = successorStateVertex.getElement(); 387 final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor(); 388 389 if (!successorStateDescriptor.isMultiVisitState() 390 && walkStateGraphContext.stateAlreadyVisited(successorState)) { 391 // This can happen if a state leads back to the state where it originated from. See for example the 392 // 'Compression' state. We return 'null' here to signal that the state can safely be ignored. 393 return null; 394 } 395 396 if (successorStateDescriptor.isNotImplemented()) { 397 StateTransitionResult.TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new StateTransitionResult.TransitionImpossibleBecauseNotImplemented( 398 successorStateDescriptor); 399 invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState, successorState, 400 transtionImpossibleBecauseNotImplemented)); 401 return transtionImpossibleBecauseNotImplemented; 402 } 403 404 final StateTransitionResult.AttemptResult transitionAttemptResult; 405 try { 406 StateTransitionResult.TransitionImpossible transitionImpossible = successorState.isTransitionToPossible( 407 walkStateGraphContext); 408 if (transitionImpossible != null) { 409 invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState, successorState, 410 transitionImpossible)); 411 return transitionImpossible; 412 } 413 414 invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(initialState, successorState)); 415 transitionAttemptResult = successorState.transitionInto(walkStateGraphContext); 416 } catch (SmackException | IOException | InterruptedException | XMPPException e) { 417 // Unwind the state here too, since this state will not be unwound by walkStateGraph(), as it will not 418 // become a predecessor state in the walk. 419 unwindState(successorState); 420 throw e; 421 } 422 if (transitionAttemptResult instanceof StateTransitionResult.Failure) { 423 StateTransitionResult.Failure transitionFailureResult = (StateTransitionResult.Failure) transitionAttemptResult; 424 invokeConnectionStateMachineListener( 425 new ConnectionStateEvent.TransitionFailed(initialState, successorState, transitionFailureResult)); 426 return transitionAttemptResult; 427 } 428 429 // If transitionAttemptResult is not an instance of TransitionFailureResult, then it has to be of type 430 // TransitionSuccessResult. 431 StateTransitionResult.Success transitionSuccessResult = (StateTransitionResult.Success) transitionAttemptResult; 432 433 currentStateVertex = successorStateVertex; 434 invokeConnectionStateMachineListener( 435 new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, transitionSuccessResult)); 436 437 return transitionSuccessResult; 438 } 439 440 @Override 441 protected void sendStanzaInternal(Stanza stanza) throws NotConnectedException, InterruptedException { 442 sendTopLevelStreamElement(stanza); 443 } 444 445 @Override 446 public void sendNonza(Nonza nonza) throws NotConnectedException, InterruptedException { 447 sendTopLevelStreamElement(nonza); 448 } 449 450 private void sendTopLevelStreamElement(TopLevelStreamElement element) throws NotConnectedException, InterruptedException { 451 final XmppClientToServerTransport transport = activeTransport; 452 if (transport == null) { 453 throw new NotConnectedException(); 454 } 455 456 outgoingElementsQueue.put(element); 457 transport.notifyAboutNewOutgoingElements(); 458 } 459 460 @Override 461 protected void shutdown() { 462 shutdown(false); 463 } 464 465 @Override 466 public synchronized void instantShutdown() { 467 shutdown(true); 468 } 469 470 @Override 471 public ModularXmppClientToServerConnectionConfiguration getConfiguration() { 472 return configuration; 473 } 474 475 private void shutdown(boolean instant) { 476 Class<? extends StateDescriptor> mandatoryIntermediateState; 477 if (instant) { 478 mandatoryIntermediateState = InstantShutdownStateDescriptor.class; 479 } else { 480 mandatoryIntermediateState = ShutdownStateDescriptor.class; 481 } 482 483 WalkStateGraphContext context = buildNewWalkTo( 484 DisconnectedStateDescriptor.class).withMandatoryIntermediateState( 485 mandatoryIntermediateState).build(); 486 487 try { 488 walkStateGraph(context); 489 } catch (IOException | SmackException | InterruptedException | XMPPException e) { 490 throw new IllegalStateException("A walk to disconnected state should never throw", e); 491 } 492 } 493 494 private SSLSession getSSLSession() { 495 final XmppClientToServerTransport transport = activeTransport; 496 if (transport == null) { 497 return null; 498 } 499 return transport.getSslSession(); 500 } 501 502 @Override 503 protected void afterFeaturesReceived() { 504 featuresReceived = true; 505 notifyWaitingThreads(); 506 } 507 508 private void parseAndProcessElement(String element) { 509 try { 510 XmlPullParser parser = PacketParserUtils.getParserFor(element); 511 512 // Skip the enclosing stream open what is guaranteed to be there. 513 parser.next(); 514 515 XmlPullParser.Event event = parser.getEventType(); 516 outerloop: while (true) { 517 switch (event) { 518 case START_ELEMENT: 519 final String name = parser.getName(); 520 // Note that we don't handle "stream" here as it's done in the splitter. 521 switch (name) { 522 case Message.ELEMENT: 523 case IQ.IQ_ELEMENT: 524 case Presence.ELEMENT: 525 try { 526 parseAndProcessStanza(parser); 527 } finally { 528 // TODO: Here would be the following stream management code. 529 // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); 530 } 531 break; 532 case "error": 533 StreamError streamError = PacketParserUtils.parseStreamError(parser, null); 534 StreamErrorException streamErrorException = new StreamErrorException(streamError); 535 currentXmppException = streamErrorException; 536 notifyWaitingThreads(); 537 throw streamErrorException; 538 case "features": 539 parseFeatures(parser); 540 afterFeaturesReceived(); 541 break; 542 default: 543 parseAndProcessNonza(parser); 544 break; 545 } 546 break; 547 case END_DOCUMENT: 548 break outerloop; 549 default: // fall out 550 } 551 event = parser.next(); 552 } 553 } catch (XmlPullParserException | IOException | InterruptedException | StreamErrorException 554 | SmackParsingException e) { 555 notifyConnectionError(e); 556 } 557 } 558 559 private synchronized void prepareToWaitForFeaturesReceived() { 560 featuresReceived = false; 561 } 562 563 private void waitForFeaturesReceived(String waitFor) 564 throws InterruptedException, SmackException, XMPPException { 565 waitForConditionOrThrowConnectionException(() -> featuresReceived, waitFor); 566 } 567 568 @Override 569 protected AbstractStreamOpen getStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) { 570 StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); 571 return streamOpenAndCloseFactory.createStreamOpen(to, from, id, lang); 572 } 573 574 private void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, 575 SmackException, XMPPException { 576 prepareToWaitForFeaturesReceived(); 577 578 // Create StreamOpen from StreamOpenAndCloseFactory via underlying transport. 579 StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); 580 CharSequence from = null; 581 CharSequence localpart = connectionInternal.connection.getConfiguration().getUsername(); 582 DomainBareJid xmppServiceDomain = getXMPPServiceDomain(); 583 if (localpart != null) { 584 from = XmppStringUtils.completeJidFrom(localpart, xmppServiceDomain); 585 } 586 AbstractStreamOpen streamOpen = streamOpenAndCloseFactory.createStreamOpen(xmppServiceDomain, from, getStreamId(), getConfiguration().getXmlLang()); 587 sendStreamOpen(streamOpen); 588 589 waitForFeaturesReceived(waitFor); 590 } 591 592 private void sendStreamOpen(AbstractStreamOpen streamOpen) throws NotConnectedException, InterruptedException { 593 sendNonza(streamOpen); 594 updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen); 595 } 596 597 public static class DisconnectedStateDescriptor extends StateDescriptor { 598 protected DisconnectedStateDescriptor() { 599 super(DisconnectedState.class, StateDescriptor.Property.finalState); 600 addSuccessor(LookupRemoteConnectionEndpointsStateDescriptor.class); 601 } 602 } 603 604 private final class DisconnectedState extends State { 605 606 private DisconnectedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 607 super(stateDescriptor, connectionInternal); 608 } 609 610 @Override 611 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { 612 synchronized (ModularXmppClientToServerConnection.this) { 613 if (inputOutputFilters.isEmpty()) { 614 previousInputOutputFilters = null; 615 } else { 616 previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size()); 617 previousInputOutputFilters.addAll(inputOutputFilters); 618 inputOutputFilters.clear(); 619 } 620 } 621 622 // Reset all states we have visited when transitioning from disconnected to authenticated. This assumes that 623 // every state after authenticated does not need to be reset. 624 ListIterator<State> it = walkFromDisconnectToAuthenticated.listIterator( 625 walkFromDisconnectToAuthenticated.size()); 626 while (it.hasPrevious()) { 627 State stateToReset = it.previous(); 628 stateToReset.resetState(); 629 } 630 walkFromDisconnectToAuthenticated = null; 631 632 return StateTransitionResult.Success.EMPTY_INSTANCE; 633 } 634 } 635 636 public static final class LookupRemoteConnectionEndpointsStateDescriptor extends StateDescriptor { 637 private LookupRemoteConnectionEndpointsStateDescriptor() { 638 super(LookupRemoteConnectionEndpointsState.class); 639 } 640 } 641 642 private final class LookupRemoteConnectionEndpointsState extends State { 643 boolean outgoingElementsQueueWasShutdown; 644 645 private LookupRemoteConnectionEndpointsState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 646 super(stateDescriptor, connectionInternal); 647 } 648 649 @Override 650 public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, 651 SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { 652 // There is a challenge here: We are going to trigger the discovery of endpoints which will run 653 // asynchronously. After a timeout, all discovered endpoints are collected. To prevent stale results from 654 // previous discover runs, the results are communicated via SmackFuture, so that we always handle the most 655 // up-to-date results. 656 657 Map<XmppClientToServerTransport, List<SmackFuture<LookupConnectionEndpointsResult, Exception>>> lookupFutures = new HashMap<>( 658 transports.size()); 659 660 final int numberOfFutures; 661 { 662 List<SmackFuture<?, ?>> allFutures = new ArrayList<>(); 663 for (XmppClientToServerTransport transport : transports.values()) { 664 // First we clear the transport of any potentially previously discovered connection endpoints. 665 transport.resetDiscoveredConnectionEndpoints(); 666 667 // Ask the transport to start the discovery of remote connection endpoints asynchronously. 668 List<SmackFuture<LookupConnectionEndpointsResult, Exception>> transportFutures = transport.lookupConnectionEndpoints(); 669 670 lookupFutures.put(transport, transportFutures); 671 allFutures.addAll(transportFutures); 672 } 673 674 numberOfFutures = allFutures.size(); 675 676 // Wait until all features are ready or if the timeout occurs. Note that we do not inspect and react the 677 // return value of SmackFuture.await() as we want to collect the LookupConnectionEndpointsFailed later. 678 SmackFuture.await(allFutures, getReplyTimeout(), TimeUnit.MILLISECONDS); 679 } 680 681 // Note that we do not pass the lookupFailures in case there is at least one successful transport. The 682 // lookup failures are also recording in LookupConnectionEndpointsSuccess, e.g. as part of 683 // RemoteXmppTcpConnectionEndpoints.Result. 684 List<LookupConnectionEndpointsFailed> lookupFailures = new ArrayList<>(numberOfFutures); 685 686 boolean atLeastOneConnectionEndpointDiscovered = false; 687 for (Map.Entry<XmppClientToServerTransport, List<SmackFuture<LookupConnectionEndpointsResult, Exception>>> entry : lookupFutures.entrySet()) { 688 XmppClientToServerTransport transport = entry.getKey(); 689 690 for (SmackFuture<LookupConnectionEndpointsResult, Exception> future : entry.getValue()) { 691 LookupConnectionEndpointsResult result = future.getIfAvailable(); 692 693 if (result == null) { 694 continue; 695 } 696 697 if (result instanceof LookupConnectionEndpointsFailed) { 698 LookupConnectionEndpointsFailed lookupFailure = (LookupConnectionEndpointsFailed) result; 699 lookupFailures.add(lookupFailure); 700 continue; 701 } 702 703 LookupConnectionEndpointsSuccess successResult = (LookupConnectionEndpointsSuccess) result; 704 705 // Arm the transport with the success result, so that its information can be used by the transport 706 // to establish the connection. 707 transport.loadConnectionEndpoints(successResult); 708 709 // Mark that the connection attempt can continue. 710 atLeastOneConnectionEndpointDiscovered = true; 711 } 712 } 713 714 if (!atLeastOneConnectionEndpointDiscovered) { 715 throw SmackException.NoEndpointsDiscoveredException.from(lookupFailures); 716 } 717 718 if (!lookupFailures.isEmpty()) { 719 // TODO: Put those non-fatal lookup failures into a sink of the connection so that the user is able to 720 // be aware of them. 721 } 722 723 // Even though the outgoing elements queue is unrelated to the lookup remote connection endpoints state, we 724 // do start the queue at this point. The transports will need it available, and we use the state's reset() 725 // function to close the queue again on failure. 726 outgoingElementsQueueWasShutdown = outgoingElementsQueue.start(); 727 728 return StateTransitionResult.Success.EMPTY_INSTANCE; 729 } 730 731 @Override 732 public void resetState() { 733 for (XmppClientToServerTransport transport : transports.values()) { 734 transport.resetDiscoveredConnectionEndpoints(); 735 } 736 737 if (outgoingElementsQueueWasShutdown) { 738 // Reset the outgoing elements queue in this state, since we also start it in this state. 739 outgoingElementsQueue.shutdown(); 740 } 741 } 742 } 743 744 public static final class ConnectedButUnauthenticatedStateDescriptor extends StateDescriptor { 745 private ConnectedButUnauthenticatedStateDescriptor() { 746 super(ConnectedButUnauthenticatedState.class, StateDescriptor.Property.finalState); 747 addSuccessor(SaslAuthenticationStateDescriptor.class); 748 addSuccessor(InstantShutdownStateDescriptor.class); 749 addSuccessor(ShutdownStateDescriptor.class); 750 } 751 } 752 753 private final class ConnectedButUnauthenticatedState extends State { 754 private ConnectedButUnauthenticatedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 755 super(stateDescriptor, connectionInternal); 756 } 757 758 @Override 759 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { 760 assert walkFromDisconnectToAuthenticated == null; 761 762 if (walkStateGraphContext.isWalksFinalState(getStateDescriptor())) { 763 // If this is the final state, then record the walk so far. 764 walkFromDisconnectToAuthenticated = walkStateGraphContext.getWalk(); 765 } 766 767 connected = true; 768 return StateTransitionResult.Success.EMPTY_INSTANCE; 769 } 770 771 @Override 772 public void resetState() { 773 connected = false; 774 } 775 } 776 777 public static final class SaslAuthenticationStateDescriptor extends StateDescriptor { 778 private SaslAuthenticationStateDescriptor() { 779 super(SaslAuthenticationState.class, "RFC 6120 § 6"); 780 addSuccessor(AuthenticatedButUnboundStateDescriptor.class); 781 } 782 } 783 784 private final class SaslAuthenticationState extends State { 785 private SaslAuthenticationState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 786 super(stateDescriptor, connectionInternal); 787 } 788 789 @Override 790 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) 791 throws IOException, SmackException, InterruptedException, XMPPException { 792 prepareToWaitForFeaturesReceived(); 793 794 LoginContext loginContext = walkStateGraphContext.getLoginContext(); 795 SASLMechanism usedSaslMechanism = authenticate(loginContext.username, loginContext.password, 796 config.getAuthzid(), getSSLSession()); 797 // authenticate() will only return if the SASL authentication was successful, but we also need to wait for 798 // the next round of stream features. 799 800 waitForFeaturesReceived("server stream features after SASL authentication"); 801 802 return new SaslAuthenticationSuccessResult(usedSaslMechanism); 803 } 804 } 805 806 public static final class SaslAuthenticationSuccessResult extends StateTransitionResult.Success { 807 private final String saslMechanismName; 808 809 private SaslAuthenticationSuccessResult(SASLMechanism usedSaslMechanism) { 810 super("SASL authentication successfull using " + usedSaslMechanism.getName()); 811 this.saslMechanismName = usedSaslMechanism.getName(); 812 } 813 814 public String getSaslMechanismName() { 815 return saslMechanismName; 816 } 817 } 818 819 public static final class AuthenticatedButUnboundStateDescriptor extends StateDescriptor { 820 private AuthenticatedButUnboundStateDescriptor() { 821 super(StateDescriptor.Property.multiVisitState); 822 addSuccessor(ResourceBindingStateDescriptor.class); 823 } 824 } 825 826 public static final class ResourceBindingStateDescriptor extends StateDescriptor { 827 private ResourceBindingStateDescriptor() { 828 super(ResourceBindingState.class, "RFC 6120 § 7"); 829 addSuccessor(AuthenticatedAndResourceBoundStateDescriptor.class); 830 } 831 } 832 833 private final class ResourceBindingState extends State { 834 private ResourceBindingState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 835 super(stateDescriptor, connectionInternal); 836 } 837 838 @Override 839 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) 840 throws IOException, SmackException, InterruptedException, XMPPException { 841 // Calling bindResourceAndEstablishSession() below requires the lastFeaturesReceived sync point to be signaled. 842 // Since we entered this state, the FSM has decided that the last features have been received, hence signal 843 // the sync point. 844 lastFeaturesReceived = true; 845 notifyWaitingThreads(); 846 847 LoginContext loginContext = walkStateGraphContext.getLoginContext(); 848 Resourcepart resource = bindResourceAndEstablishSession(loginContext.resource); 849 850 // TODO: This should be a field in the Stream Management (SM) module. Here should be hook which the SM 851 // module can use to set the field instead. 852 streamResumed = false; 853 854 return new ResourceBoundResult(resource, loginContext.resource); 855 } 856 } 857 858 public static final class ResourceBoundResult extends StateTransitionResult.Success { 859 private final Resourcepart resource; 860 861 private ResourceBoundResult(Resourcepart boundResource, Resourcepart requestedResource) { 862 super("Resource '" + boundResource + "' bound (requested: '" + requestedResource + "')"); 863 this.resource = boundResource; 864 } 865 866 public Resourcepart getResource() { 867 return resource; 868 } 869 } 870 871 private boolean compressionEnabled; 872 873 @Override 874 public boolean isUsingCompression() { 875 return compressionEnabled; 876 } 877 878 public static final class AuthenticatedAndResourceBoundStateDescriptor extends StateDescriptor { 879 private AuthenticatedAndResourceBoundStateDescriptor() { 880 super(AuthenticatedAndResourceBoundState.class, StateDescriptor.Property.finalState); 881 addSuccessor(InstantShutdownStateDescriptor.class); 882 addSuccessor(ShutdownStateDescriptor.class); 883 } 884 } 885 886 private final class AuthenticatedAndResourceBoundState extends State { 887 private AuthenticatedAndResourceBoundState(StateDescriptor stateDescriptor, 888 ModularXmppClientToServerConnectionInternal connectionInternal) { 889 super(stateDescriptor, connectionInternal); 890 } 891 892 @Override 893 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) 894 throws NotConnectedException, InterruptedException { 895 if (walkFromDisconnectToAuthenticated != null) { 896 // If there was already a previous walk to ConnectedButUnauthenticated, then the context of the current 897 // walk must not start from the 'Disconnected' state. 898 assert walkStateGraphContext.getWalk().get(0).getStateDescriptor().getClass() 899 != DisconnectedStateDescriptor.class; 900 // Append the current walk to the previous one. 901 walkStateGraphContext.appendWalkTo(walkFromDisconnectToAuthenticated); 902 } else { 903 walkFromDisconnectToAuthenticated = new ArrayList<>( 904 walkStateGraphContext.getWalkLength() + 1); 905 walkStateGraphContext.appendWalkTo(walkFromDisconnectToAuthenticated); 906 } 907 walkFromDisconnectToAuthenticated.add(this); 908 909 afterSuccessfulLogin(streamResumed); 910 911 return StateTransitionResult.Success.EMPTY_INSTANCE; 912 } 913 914 @Override 915 public void resetState() { 916 authenticated = false; 917 } 918 } 919 920 static final class ShutdownStateDescriptor extends StateDescriptor { 921 private ShutdownStateDescriptor() { 922 super(ShutdownState.class); 923 addSuccessor(CloseConnectionStateDescriptor.class); 924 } 925 } 926 927 private final class ShutdownState extends State { 928 private ShutdownState(StateDescriptor stateDescriptor, 929 ModularXmppClientToServerConnectionInternal connectionInternal) { 930 super(stateDescriptor, connectionInternal); 931 } 932 933 @Override 934 public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { 935 ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); 936 return null; 937 } 938 939 @Override 940 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { 941 closingStreamReceived = false; 942 943 StreamOpenAndCloseFactory openAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); 944 AbstractStreamClose closeStreamElement = openAndCloseFactory.createStreamClose(); 945 boolean streamCloseIssued = outgoingElementsQueue.offerAndShutdown(closeStreamElement); 946 947 if (streamCloseIssued) { 948 activeTransport.notifyAboutNewOutgoingElements(); 949 950 boolean successfullyReceivedStreamClose = waitForClosingStreamTagFromServer(); 951 952 if (successfullyReceivedStreamClose) { 953 for (Iterator<XmppInputOutputFilter> it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) { 954 XmppInputOutputFilter filter = it.next(); 955 filter.closeInputOutput(); 956 } 957 958 // Closing the filters may produced new outgoing data. Notify the transport about it. 959 activeTransport.afterFiltersClosed(); 960 961 for (Iterator<XmppInputOutputFilter> it = connectionInternal.getXmppInputOutputFilterBeginIterator(); it.hasNext();) { 962 XmppInputOutputFilter filter = it.next(); 963 try { 964 filter.waitUntilInputOutputClosed(); 965 } catch (IOException | CertificateException | InterruptedException | SmackException | XMPPException e) { 966 LOGGER.log(Level.WARNING, "waitUntilInputOutputClosed() threw", e); 967 } 968 } 969 970 // For correctness we set authenticated to false here, even though we will later again set it to 971 // false in the disconnected state. 972 authenticated = false; 973 } 974 } 975 976 return StateTransitionResult.Success.EMPTY_INSTANCE; 977 } 978 } 979 980 static final class InstantShutdownStateDescriptor extends StateDescriptor { 981 private InstantShutdownStateDescriptor() { 982 super(InstantShutdownState.class); 983 addSuccessor(CloseConnectionStateDescriptor.class); 984 } 985 } 986 987 private static final class InstantShutdownState extends NoOpState { 988 private InstantShutdownState(ModularXmppClientToServerConnection connection, StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 989 super(connection, stateDescriptor, connectionInternal); 990 } 991 992 @Override 993 public StateTransitionResult.TransitionImpossible isTransitionToPossible(WalkStateGraphContext walkStateGraphContext) { 994 ensureNotOnOurWayToAuthenticatedAndResourceBound(walkStateGraphContext); 995 return null; 996 } 997 } 998 999 private static final class CloseConnectionStateDescriptor extends StateDescriptor { 1000 private CloseConnectionStateDescriptor() { 1001 super(CloseConnectionState.class); 1002 addSuccessor(DisconnectedStateDescriptor.class); 1003 } 1004 } 1005 1006 private final class CloseConnectionState extends State { 1007 private CloseConnectionState(StateDescriptor stateDescriptor, 1008 ModularXmppClientToServerConnectionInternal connectionInternal) { 1009 super(stateDescriptor, connectionInternal); 1010 } 1011 1012 @Override 1013 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { 1014 activeTransport.disconnect(); 1015 activeTransport = null; 1016 1017 authenticated = connected = false; 1018 1019 return StateTransitionResult.Success.EMPTY_INSTANCE; 1020 } 1021 } 1022 1023 public void addConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { 1024 connectionStateMachineListeners.add(connectionStateMachineListener); 1025 } 1026 1027 public boolean removeConnectionStateMachineListener(ConnectionStateMachineListener connectionStateMachineListener) { 1028 return connectionStateMachineListeners.remove(connectionStateMachineListener); 1029 } 1030 1031 private void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { 1032 if (connectionStateMachineListeners.isEmpty()) { 1033 return; 1034 } 1035 1036 ASYNC_BUT_ORDERED.performAsyncButOrdered(this, () -> { 1037 for (ConnectionStateMachineListener connectionStateMachineListener : connectionStateMachineListeners) { 1038 connectionStateMachineListener.onConnectionStateEvent(connectionStateEvent, this); 1039 } 1040 }); 1041 } 1042 1043 @Override 1044 public boolean isSecureConnection() { 1045 final XmppClientToServerTransport transport = activeTransport; 1046 if (transport == null) { 1047 return false; 1048 } 1049 return transport.isTransportSecured(); 1050 } 1051 1052 @Override 1053 protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { 1054 WalkStateGraphContext walkStateGraphContext = buildNewWalkTo(ConnectedButUnauthenticatedStateDescriptor.class) 1055 .build(); 1056 walkStateGraph(walkStateGraphContext); 1057 } 1058 1059 private Map<String, Object> getFilterStats() { 1060 Collection<XmppInputOutputFilter> filters; 1061 synchronized (this) { 1062 if (inputOutputFilters.isEmpty() && previousInputOutputFilters != null) { 1063 filters = previousInputOutputFilters; 1064 } else { 1065 filters = inputOutputFilters; 1066 } 1067 } 1068 1069 Map<String, Object> filterStats = new HashMap<>(filters.size()); 1070 for (XmppInputOutputFilter xmppInputOutputFilter : filters) { 1071 Object stats = xmppInputOutputFilter.getStats(); 1072 String filterName = xmppInputOutputFilter.getFilterName(); 1073 1074 filterStats.put(filterName, stats); 1075 } 1076 1077 return filterStats; 1078 } 1079 1080 public Stats getStats() { 1081 Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats = new HashMap<>( 1082 transports.size()); 1083 for (Map.Entry<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport> entry : transports.entrySet()) { 1084 XmppClientToServerTransport.Stats transportStats = entry.getValue().getStats(); 1085 1086 transportsStats.put(entry.getKey(), transportStats); 1087 } 1088 1089 Map<String, Object> filterStats = getFilterStats(); 1090 1091 return new Stats(transportsStats, filterStats); 1092 } 1093 1094 public static final class Stats extends AbstractStats { 1095 public final Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats; 1096 public final Map<String, Object> filtersStats; 1097 1098 private Stats(Map<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> transportsStats, 1099 Map<String, Object> filtersStats) { 1100 this.transportsStats = Collections.unmodifiableMap(transportsStats); 1101 this.filtersStats = Collections.unmodifiableMap(filtersStats); 1102 } 1103 1104 @Override 1105 public void appendStatsTo(ExtendedAppendable appendable) throws IOException { 1106 StringUtils.appendHeading(appendable, "Connection stats", '#').append('\n'); 1107 1108 for (Map.Entry<Class<? extends ModularXmppClientToServerConnectionModuleDescriptor>, XmppClientToServerTransport.Stats> entry : transportsStats.entrySet()) { 1109 Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> transportClass = entry.getKey(); 1110 XmppClientToServerTransport.Stats stats = entry.getValue(); 1111 1112 StringUtils.appendHeading(appendable, transportClass.getName()); 1113 if (stats != null) { 1114 appendable.append(stats.toString()); 1115 } else { 1116 appendable.append("No stats available."); 1117 } 1118 appendable.append('\n'); 1119 } 1120 1121 for (Map.Entry<String, Object> entry : filtersStats.entrySet()) { 1122 String filterName = entry.getKey(); 1123 Object filterStats = entry.getValue(); 1124 1125 StringUtils.appendHeading(appendable, filterName); 1126 appendable.append(filterStats.toString()).append('\n'); 1127 } 1128 } 1129 1130 } 1131}