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.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 protected 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 void setCurrentConnectionExceptionAndNotify(Exception exception) { 143 ModularXmppClientToServerConnection.this.setCurrentConnectionExceptionAndNotify(exception); 144 } 145 146 @Override 147 public void onStreamOpen(XmlPullParser parser) { 148 ModularXmppClientToServerConnection.this.onStreamOpen(parser); 149 } 150 151 @Override 152 public void onStreamClosed() { 153 ModularXmppClientToServerConnection.this.closingStreamReceived = true; 154 notifyWaitingThreads(); 155 } 156 157 @Override 158 public void fireFirstLevelElementSendListeners(TopLevelStreamElement element) { 159 ModularXmppClientToServerConnection.this.firePacketSendingListeners(element); 160 } 161 162 @Override 163 public void invokeConnectionStateMachineListener(ConnectionStateEvent connectionStateEvent) { 164 ModularXmppClientToServerConnection.this.invokeConnectionStateMachineListener(connectionStateEvent); 165 } 166 167 @Override 168 public XmlEnvironment getOutgoingStreamXmlEnvironment() { 169 return outgoingStreamXmlEnvironment; 170 } 171 172 @Override 173 public void addXmppInputOutputFilter(XmppInputOutputFilter xmppInputOutputFilter) { 174 inputOutputFilters.add(0, xmppInputOutputFilter); 175 } 176 177 @Override 178 public ListIterator<XmppInputOutputFilter> getXmppInputOutputFilterBeginIterator() { 179 return inputOutputFilters.listIterator(); 180 } 181 182 @Override 183 public ListIterator<XmppInputOutputFilter> getXmppInputOutputFilterEndIterator() { 184 return inputOutputFilters.listIterator(inputOutputFilters.size()); 185 } 186 187 @Override 188 public void waitForFeaturesReceived(String waitFor) throws InterruptedException, SmackException, XMPPException { 189 ModularXmppClientToServerConnection.this.waitForFeaturesReceived(waitFor); 190 } 191 192 @Override 193 public void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, 194 SmackException, XMPPException { 195 ModularXmppClientToServerConnection.this.newStreamOpenWaitForFeaturesSequence(waitFor); 196 } 197 198 @Override 199 public SmackTlsContext getSmackTlsContext() { 200 return ModularXmppClientToServerConnection.this.getSmackTlsContext(); 201 } 202 203 @Override 204 public <SN extends Nonza, FN extends Nonza> SN sendAndWaitForResponse(Nonza nonza, Class<SN> successNonzaClass, 205 Class<FN> failedNonzaClass) throws NoResponseException, NotConnectedException, FailedNonzaException, InterruptedException { 206 return ModularXmppClientToServerConnection.this.sendAndWaitForResponse(nonza, successNonzaClass, failedNonzaClass); 207 } 208 209 @Override 210 public void asyncGo(Runnable runnable) { 211 AbstractXMPPConnection.asyncGo(runnable); 212 } 213 214 @Override 215 public void waitForConditionOrThrowConnectionException(Supplier<Boolean> condition, String waitFor) 216 throws InterruptedException, SmackException, XMPPException { 217 ModularXmppClientToServerConnection.this.waitForConditionOrThrowConnectionException(condition, waitFor); 218 } 219 220 @Override 221 public void notifyWaitingThreads() { 222 ModularXmppClientToServerConnection.this.notifyWaitingThreads(); 223 } 224 225 @Override 226 public void setCompressionEnabled(boolean compressionEnabled) { 227 ModularXmppClientToServerConnection.this.compressionEnabled = compressionEnabled; 228 } 229 230 @Override 231 public void setTransport(XmppClientToServerTransport xmppTransport) { 232 ModularXmppClientToServerConnection.this.activeTransport = xmppTransport; 233 ModularXmppClientToServerConnection.this.connected = true; 234 } 235 236 }; 237 238 // Construct the modules from the module descriptor. We do this before constructing the state graph, as the 239 // modules are sometimes used to construct the states. 240 for (ModularXmppClientToServerConnectionModuleDescriptor moduleDescriptor : configuration.moduleDescriptors) { 241 Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> moduleDescriptorClass = moduleDescriptor.getClass(); 242 ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor> connectionModule = moduleDescriptor.constructXmppConnectionModule(connectionInternal); 243 connectionModules.put(moduleDescriptorClass, connectionModule); 244 245 XmppClientToServerTransport transport = connectionModule.getTransport(); 246 // Not every module may provide a transport. 247 if (transport != null) { 248 transports.put(moduleDescriptorClass, transport); 249 } 250 } 251 252 GraphVertex<StateDescriptor> initialStateDescriptorVertex = configuration.initialStateDescriptorVertex; 253 // Convert the graph of state descriptors to a graph of states, bound to this very connection. 254 currentStateVertex = StateDescriptorGraph.convertToStateGraph(initialStateDescriptorVertex, connectionInternal); 255 } 256 257 @SuppressWarnings("unchecked") 258 public <CM extends ModularXmppClientToServerConnectionModule<? extends ModularXmppClientToServerConnectionModuleDescriptor>> CM getConnectionModuleFor( 259 Class<? extends ModularXmppClientToServerConnectionModuleDescriptor> descriptorClass) { 260 return (CM) connectionModules.get(descriptorClass); 261 } 262 263 @Override 264 protected void loginInternal(String username, String password, Resourcepart resource) 265 throws XMPPException, SmackException, IOException, InterruptedException { 266 WalkStateGraphContext walkStateGraphContext = buildNewWalkTo( 267 AuthenticatedAndResourceBoundStateDescriptor.class).withLoginContext(username, password, 268 resource).build(); 269 walkStateGraph(walkStateGraphContext); 270 } 271 272 protected WalkStateGraphContext.Builder buildNewWalkTo(Class<? extends StateDescriptor> finalStateClass) { 273 return WalkStateGraphContext.builder(currentStateVertex.getElement().getStateDescriptor().getClass(), finalStateClass); 274 } 275 276 /** 277 * Unwind the state. This will revert the effects of the state by calling {@link State#resetState()} prior issuing a 278 * connection state event of {@link ConnectionStateEvent#StateRevertBackwardsWalk}. 279 * 280 * @param revertedState the state which is going to get reverted. 281 */ 282 private void unwindState(State revertedState) { 283 invokeConnectionStateMachineListener(new ConnectionStateEvent.StateRevertBackwardsWalk(revertedState)); 284 revertedState.resetState(); 285 } 286 287 protected void walkStateGraph(WalkStateGraphContext walkStateGraphContext) 288 throws XMPPException, IOException, SmackException, InterruptedException { 289 // Save a copy of the current state 290 GraphVertex<State> previousStateVertex = currentStateVertex; 291 try { 292 walkStateGraphInternal(walkStateGraphContext); 293 } catch (IOException | SmackException | InterruptedException | XMPPException e) { 294 currentStateVertex = previousStateVertex; 295 // Unwind the state. 296 State revertedState = currentStateVertex.getElement(); 297 unwindState(revertedState); 298 throw e; 299 } 300 } 301 302 private void walkStateGraphInternal(WalkStateGraphContext walkStateGraphContext) 303 throws IOException, SmackException, InterruptedException, XMPPException { 304 // Save a copy of the current state 305 final GraphVertex<State> initialStateVertex = currentStateVertex; 306 final State initialState = initialStateVertex.getElement(); 307 final StateDescriptor initialStateDescriptor = initialState.getStateDescriptor(); 308 309 walkStateGraphContext.recordWalkTo(initialState); 310 311 // Check if this is the walk's final state. 312 if (walkStateGraphContext.isWalksFinalState(initialStateDescriptor)) { 313 // If this is used as final state, then it should be marked as such. 314 assert initialStateDescriptor.isFinalState(); 315 316 // We reached the final state. 317 invokeConnectionStateMachineListener(new ConnectionStateEvent.FinalStateReached(initialState)); 318 return; 319 } 320 321 List<GraphVertex<State>> outgoingStateEdges = initialStateVertex.getOutgoingEdges(); 322 323 // See if we need to handle mandatory intermediate states. 324 GraphVertex<State> mandatoryIntermediateStateVertex = walkStateGraphContext.maybeReturnMandatoryImmediateState(outgoingStateEdges); 325 if (mandatoryIntermediateStateVertex != null) { 326 StateTransitionResult reason = attemptEnterState(mandatoryIntermediateStateVertex, walkStateGraphContext); 327 328 if (reason instanceof StateTransitionResult.Success) { 329 walkStateGraph(walkStateGraphContext); 330 return; 331 } 332 333 // We could not enter a mandatory intermediate state. Throw here. 334 throw new StateMachineException.SmackMandatoryStateFailedException( 335 mandatoryIntermediateStateVertex.getElement(), reason); 336 } 337 338 for (Iterator<GraphVertex<State>> it = outgoingStateEdges.iterator(); it.hasNext();) { 339 GraphVertex<State> successorStateVertex = it.next(); 340 State successorState = successorStateVertex.getElement(); 341 342 // Ignore successorStateVertex if the only way to the final state is via the initial state. This happens 343 // typically if we are in the ConnectedButUnauthenticated state on the way to ResourceboundAndAuthenticated, 344 // where we do not want to walk via InstantShutdown/Shtudown in a cycle over the initial state towards this 345 // state. 346 if (walkStateGraphContext.wouldCauseCycle(successorStateVertex)) { 347 // Ignore this successor. 348 invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionIgnoredDueCycle(initialStateVertex, successorStateVertex)); 349 } else { 350 StateTransitionResult result = attemptEnterState(successorStateVertex, walkStateGraphContext); 351 352 if (result instanceof StateTransitionResult.Success) { 353 break; 354 } 355 356 // If attemptEnterState did not throw and did not return a value of type TransitionSuccessResult, then we 357 // just record this value and go on from there. Note that reason may be null, which is returned by 358 // attemptEnterState in case the state was already successfully handled. If this is the case, then we don't 359 // record it. 360 if (result != null) { 361 walkStateGraphContext.recordFailedState(successorState, result); 362 } 363 } 364 365 if (!it.hasNext()) { 366 throw StateMachineException.SmackStateGraphDeadEndException.from(walkStateGraphContext, 367 initialStateVertex); 368 } 369 } 370 371 // Walk the state graph by recursion. 372 walkStateGraph(walkStateGraphContext); 373 } 374 375 /** 376 * Attempt to enter a state. Note that this method may return <code>null</code> if this state can be safely ignored. 377 * 378 * @param successorStateVertex the successor state vertex. 379 * @param walkStateGraphContext the "walk state graph" context. 380 * @return A state transition result or <code>null</code> if this state can be ignored. 381 * @throws SmackException if Smack detected an exceptional situation. 382 * @throws XMPPException if an XMPP protocol error was received. 383 * @throws IOException if an I/O error occurred. 384 * @throws InterruptedException if the calling thread was interrupted. 385 */ 386 private StateTransitionResult attemptEnterState(GraphVertex<State> successorStateVertex, 387 WalkStateGraphContext walkStateGraphContext) throws SmackException, XMPPException, 388 IOException, InterruptedException { 389 final GraphVertex<State> initialStateVertex = currentStateVertex; 390 final State initialState = initialStateVertex.getElement(); 391 final State successorState = successorStateVertex.getElement(); 392 final StateDescriptor successorStateDescriptor = successorState.getStateDescriptor(); 393 394 if (!successorStateDescriptor.isMultiVisitState() 395 && walkStateGraphContext.stateAlreadyVisited(successorState)) { 396 // This can happen if a state leads back to the state where it originated from. See for example the 397 // 'Compression' state. We return 'null' here to signal that the state can safely be ignored. 398 return null; 399 } 400 401 if (successorStateDescriptor.isNotImplemented()) { 402 StateTransitionResult.TransitionImpossibleBecauseNotImplemented transtionImpossibleBecauseNotImplemented = new StateTransitionResult.TransitionImpossibleBecauseNotImplemented( 403 successorStateDescriptor); 404 invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState, successorState, 405 transtionImpossibleBecauseNotImplemented)); 406 return transtionImpossibleBecauseNotImplemented; 407 } 408 409 final StateTransitionResult.AttemptResult transitionAttemptResult; 410 try { 411 StateTransitionResult.TransitionImpossible transitionImpossible = successorState.isTransitionToPossible( 412 walkStateGraphContext); 413 if (transitionImpossible != null) { 414 invokeConnectionStateMachineListener(new ConnectionStateEvent.TransitionNotPossible(initialState, successorState, 415 transitionImpossible)); 416 return transitionImpossible; 417 } 418 419 invokeConnectionStateMachineListener(new ConnectionStateEvent.AboutToTransitionInto(initialState, successorState)); 420 transitionAttemptResult = successorState.transitionInto(walkStateGraphContext); 421 } catch (SmackException | IOException | InterruptedException | XMPPException e) { 422 // Unwind the state here too, since this state will not be unwound by walkStateGraph(), as it will not 423 // become a predecessor state in the walk. 424 unwindState(successorState); 425 throw e; 426 } 427 if (transitionAttemptResult instanceof StateTransitionResult.Failure) { 428 StateTransitionResult.Failure transitionFailureResult = (StateTransitionResult.Failure) transitionAttemptResult; 429 invokeConnectionStateMachineListener( 430 new ConnectionStateEvent.TransitionFailed(initialState, successorState, transitionFailureResult)); 431 return transitionAttemptResult; 432 } 433 434 // If transitionAttemptResult is not an instance of TransitionFailureResult, then it has to be of type 435 // TransitionSuccessResult. 436 StateTransitionResult.Success transitionSuccessResult = (StateTransitionResult.Success) transitionAttemptResult; 437 438 currentStateVertex = successorStateVertex; 439 invokeConnectionStateMachineListener( 440 new ConnectionStateEvent.SuccessfullyTransitionedInto(successorState, transitionSuccessResult)); 441 442 return transitionSuccessResult; 443 } 444 445 @Override 446 protected void sendStanzaInternal(Stanza stanza) throws NotConnectedException, InterruptedException { 447 sendTopLevelStreamElement(stanza); 448 } 449 450 @Override 451 public void sendNonza(Nonza nonza) throws NotConnectedException, InterruptedException { 452 sendTopLevelStreamElement(nonza); 453 } 454 455 private void sendTopLevelStreamElement(TopLevelStreamElement element) throws NotConnectedException, InterruptedException { 456 final XmppClientToServerTransport transport = activeTransport; 457 if (transport == null) { 458 throw new NotConnectedException(); 459 } 460 461 outgoingElementsQueue.put(element); 462 transport.notifyAboutNewOutgoingElements(); 463 } 464 465 @Override 466 protected void shutdown() { 467 shutdown(false); 468 } 469 470 @Override 471 public synchronized void instantShutdown() { 472 shutdown(true); 473 } 474 475 @Override 476 public ModularXmppClientToServerConnectionConfiguration getConfiguration() { 477 return configuration; 478 } 479 480 private void shutdown(boolean instant) { 481 Class<? extends StateDescriptor> mandatoryIntermediateState; 482 if (instant) { 483 mandatoryIntermediateState = InstantShutdownStateDescriptor.class; 484 } else { 485 mandatoryIntermediateState = ShutdownStateDescriptor.class; 486 } 487 488 WalkStateGraphContext context = buildNewWalkTo( 489 DisconnectedStateDescriptor.class).withMandatoryIntermediateState( 490 mandatoryIntermediateState).build(); 491 492 try { 493 walkStateGraph(context); 494 } catch (IOException | SmackException | InterruptedException | XMPPException e) { 495 throw new IllegalStateException("A walk to disconnected state should never throw", e); 496 } 497 } 498 499 protected SSLSession getSSLSession() { 500 final XmppClientToServerTransport transport = activeTransport; 501 if (transport == null) { 502 return null; 503 } 504 return transport.getSslSession(); 505 } 506 507 @Override 508 protected void afterFeaturesReceived() { 509 featuresReceived = true; 510 notifyWaitingThreads(); 511 } 512 513 protected void parseAndProcessElement(String element) { 514 try { 515 XmlPullParser parser = PacketParserUtils.getParserFor(element); 516 517 // Skip the enclosing stream open what is guaranteed to be there. 518 parser.next(); 519 520 XmlPullParser.Event event = parser.getEventType(); 521 outerloop: while (true) { 522 switch (event) { 523 case START_ELEMENT: 524 final String name = parser.getName(); 525 // Note that we don't handle "stream" here as it's done in the splitter. 526 switch (name) { 527 case Message.ELEMENT: 528 case IQ.IQ_ELEMENT: 529 case Presence.ELEMENT: 530 try { 531 parseAndProcessStanza(parser); 532 } finally { 533 // TODO: Here would be the following stream management code. 534 // clientHandledStanzasCount = SMUtils.incrementHeight(clientHandledStanzasCount); 535 } 536 break; 537 case "error": 538 StreamError streamError = PacketParserUtils.parseStreamError(parser, null); 539 StreamErrorException streamErrorException = new StreamErrorException(streamError); 540 currentXmppException = streamErrorException; 541 notifyWaitingThreads(); 542 throw streamErrorException; 543 case "features": 544 parseFeatures(parser); 545 afterFeaturesReceived(); 546 break; 547 default: 548 parseAndProcessNonza(parser); 549 break; 550 } 551 break; 552 case END_DOCUMENT: 553 break outerloop; 554 default: // fall out 555 } 556 event = parser.next(); 557 } 558 } catch (XmlPullParserException | IOException | InterruptedException | StreamErrorException 559 | SmackParsingException e) { 560 notifyConnectionError(e); 561 } 562 } 563 564 protected synchronized void prepareToWaitForFeaturesReceived() { 565 featuresReceived = false; 566 } 567 568 protected void waitForFeaturesReceived(String waitFor) 569 throws InterruptedException, SmackException, XMPPException { 570 waitForConditionOrThrowConnectionException(() -> featuresReceived, waitFor); 571 } 572 573 @Override 574 protected AbstractStreamOpen getStreamOpen(CharSequence to, CharSequence from, String id, String lang) { 575 StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); 576 return streamOpenAndCloseFactory.createStreamOpen(to, from, id, lang); 577 } 578 579 protected void newStreamOpenWaitForFeaturesSequence(String waitFor) throws InterruptedException, 580 SmackException, XMPPException { 581 prepareToWaitForFeaturesReceived(); 582 583 // Create StreamOpen from StreamOpenAndCloseFactory via underlying transport. 584 StreamOpenAndCloseFactory streamOpenAndCloseFactory = activeTransport.getStreamOpenAndCloseFactory(); 585 CharSequence from = null; 586 CharSequence localpart = connectionInternal.connection.getConfiguration().getUsername(); 587 DomainBareJid xmppServiceDomain = getXMPPServiceDomain(); 588 if (localpart != null) { 589 from = XmppStringUtils.completeJidFrom(localpart, xmppServiceDomain); 590 } 591 AbstractStreamOpen streamOpen = streamOpenAndCloseFactory.createStreamOpen(xmppServiceDomain, from, getStreamId(), getConfiguration().getXmlLang()); 592 sendStreamOpen(streamOpen); 593 594 waitForFeaturesReceived(waitFor); 595 } 596 597 private void sendStreamOpen(AbstractStreamOpen streamOpen) throws NotConnectedException, InterruptedException { 598 sendNonza(streamOpen); 599 updateOutgoingStreamXmlEnvironmentOnStreamOpen(streamOpen); 600 } 601 602 public static class DisconnectedStateDescriptor extends StateDescriptor { 603 protected DisconnectedStateDescriptor() { 604 super(DisconnectedState.class, StateDescriptor.Property.finalState); 605 addSuccessor(LookupRemoteConnectionEndpointsStateDescriptor.class); 606 } 607 } 608 609 private final class DisconnectedState extends State { 610 611 private DisconnectedState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 612 super(stateDescriptor, connectionInternal); 613 } 614 615 @Override 616 public StateTransitionResult.AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) { 617 synchronized (ModularXmppClientToServerConnection.this) { 618 if (inputOutputFilters.isEmpty()) { 619 previousInputOutputFilters = null; 620 } else { 621 previousInputOutputFilters = new ArrayList<>(inputOutputFilters.size()); 622 previousInputOutputFilters.addAll(inputOutputFilters); 623 inputOutputFilters.clear(); 624 } 625 } 626 627 // Reset all states we have visited when transitioning from disconnected to authenticated. This assumes that 628 // every state after authenticated does not need to be reset. 629 ListIterator<State> it = walkFromDisconnectToAuthenticated.listIterator( 630 walkFromDisconnectToAuthenticated.size()); 631 while (it.hasPrevious()) { 632 State stateToReset = it.previous(); 633 stateToReset.resetState(); 634 } 635 walkFromDisconnectToAuthenticated = null; 636 637 return StateTransitionResult.Success.EMPTY_INSTANCE; 638 } 639 } 640 641 public static final class LookupRemoteConnectionEndpointsStateDescriptor extends StateDescriptor { 642 private LookupRemoteConnectionEndpointsStateDescriptor() { 643 super(LookupRemoteConnectionEndpointsState.class); 644 } 645 } 646 647 private final class LookupRemoteConnectionEndpointsState extends State { 648 boolean outgoingElementsQueueWasShutdown; 649 650 private LookupRemoteConnectionEndpointsState(StateDescriptor stateDescriptor, ModularXmppClientToServerConnectionInternal connectionInternal) { 651 super(stateDescriptor, connectionInternal); 652 } 653 654 @Override 655 public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws XMPPErrorException, 656 SASLErrorException, IOException, SmackException, InterruptedException, FailedNonzaException { 657 // There is a challenge here: We are going to trigger the discovery of endpoints which will run 658 // asynchronously. After a timeout, all discovered endpoints are collected. To prevent stale results from 659 // previous discover runs, the results are communicated via SmackFuture, so that we always handle the most 660 // up-to-date results. 661 662 Map<XmppClientToServerTransport, List<SmackFuture<LookupConnectionEndpointsResult, Exception>>> lookupFutures = new HashMap<>( 663 transports.size()); 664 665 final int numberOfFutures; 666 { 667 List<SmackFuture<?, ?>> allFutures = new ArrayList<>(); 668 for (XmppClientToServerTransport transport : transports.values()) { 669 // First we clear the transport of any potentially previously discovered connection endpoints. 670 transport.resetDiscoveredConnectionEndpoints(); 671 672 // Ask the transport to start the discovery of remote connection endpoints asynchronously. 673 List<SmackFuture<LookupConnectionEndpointsResult, Exception>> transportFutures = transport.lookupConnectionEndpoints(); 674 675 lookupFutures.put(transport, transportFutures); 676 allFutures.addAll(transportFutures); 677 } 678 679 numberOfFutures = allFutures.size(); 680 681 // Wait until all features are ready or if the timeout occurs. Note that we do not inspect and react the 682 // return value of SmackFuture.await() as we want to collect the LookupConnectionEndpointsFailed later. 683 SmackFuture.await(allFutures, getReplyTimeout(), TimeUnit.MILLISECONDS); 684 } 685 686 // Note that we do not pass the lookupFailures in case there is at least one successful transport. The 687 // lookup failures are also recording in LookupConnectionEndpointsSuccess, e.g. as part of 688 // RemoteXmppTcpConnectionEndpoints.Result. 689 List<LookupConnectionEndpointsFailed> lookupFailures = new ArrayList<>(numberOfFutures); 690 691 boolean atLeastOneConnectionEndpointDiscovered = false; 692 for (Map.Entry<XmppClientToServerTransport, List<SmackFuture<LookupConnectionEndpointsResult, Exception>>> entry : lookupFutures.entrySet()) { 693 XmppClientToServerTransport transport = entry.getKey(); 694 695 for (SmackFuture<LookupConnectionEndpointsResult, Exception> future : entry.getValue()) { 696 LookupConnectionEndpointsResult result = future.getIfAvailable(); 697 698 if (result == null) { 699 continue; 700 } 701 702 if (result instanceof LookupConnectionEndpointsFailed) { 703 LookupConnectionEndpointsFailed lookupFailure = (LookupConnectionEndpointsFailed) result; 704 lookupFailures.add(lookupFailure); 705 continue; 706 } 707 708 LookupConnectionEndpointsSuccess successResult = (LookupConnectionEndpointsSuccess) result; 709 710 // Arm the transport with the success result, so that its information can be used by the transport 711 // to establish the connection. 712 transport.loadConnectionEndpoints(successResult); 713 714 // Mark that the connection attempt can continue. 715 atLeastOneConnectionEndpointDiscovered = true; 716 } 717 } 718 719 if (!atLeastOneConnectionEndpointDiscovered) { 720 throw SmackException.NoEndpointsDiscoveredException.from(lookupFailures); 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 protected 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 protected 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 appendable.append(stats.toString()).append('\n'); 1114 } 1115 1116 for (Map.Entry<String, Object> entry : filtersStats.entrySet()) { 1117 String filterName = entry.getKey(); 1118 Object filterStats = entry.getValue(); 1119 1120 StringUtils.appendHeading(appendable, filterName); 1121 appendable.append(filterStats.toString()).append('\n'); 1122 } 1123 } 1124 1125 } 1126}