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