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