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