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}