001/**
002 *
003 * Copyright 2020 Aditya Borikar, 2020-2023 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.websocket;
018
019import java.util.Collections;
020import java.util.List;
021import java.util.Queue;
022
023import javax.net.ssl.SSLSession;
024
025import org.jivesoftware.smack.AsyncButOrdered;
026import org.jivesoftware.smack.SmackException;
027import org.jivesoftware.smack.SmackException.NoResponseException;
028import org.jivesoftware.smack.SmackException.NotConnectedException;
029import org.jivesoftware.smack.SmackFuture;
030import org.jivesoftware.smack.SmackFuture.InternalSmackFuture;
031import org.jivesoftware.smack.XMPPException;
032import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor;
033import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.LookupRemoteConnectionEndpointsStateDescriptor;
034import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
035import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule;
036import org.jivesoftware.smack.c2s.StreamOpenAndCloseFactory;
037import org.jivesoftware.smack.c2s.XmppClientToServerTransport;
038import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
039import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext;
040import org.jivesoftware.smack.fsm.State;
041import org.jivesoftware.smack.fsm.StateDescriptor;
042import org.jivesoftware.smack.fsm.StateTransitionResult;
043import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult;
044import org.jivesoftware.smack.packet.AbstractStreamClose;
045import org.jivesoftware.smack.packet.AbstractStreamOpen;
046import org.jivesoftware.smack.packet.TopLevelStreamElement;
047import org.jivesoftware.smack.util.StringUtils;
048import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure;
049import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints;
050import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement;
051import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement;
052import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
053import org.jivesoftware.smack.websocket.impl.WebSocketFactory;
054import org.jivesoftware.smack.websocket.impl.WebSocketFactoryService;
055import org.jivesoftware.smack.websocket.rce.InsecureWebSocketRemoteConnectionEndpoint;
056import org.jivesoftware.smack.websocket.rce.SecureWebSocketRemoteConnectionEndpoint;
057import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
058import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup;
059import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup.Result;
060
061import org.jxmpp.jid.DomainBareJid;
062
063/**
064 * The websocket transport module that goes with Smack's modular architecture.
065 */
066public final class XmppWebSocketTransportModule
067                extends ModularXmppClientToServerConnectionModule<XmppWebSocketTransportModuleDescriptor> {
068
069    private static final int WEBSOCKET_NORMAL_CLOSURE = 1000;
070
071    private static final int[] KNOWN_SECURE_PORTS = new int[] {5443, 5281, 7443};
072    private static final int[] KNOWN_INSECURE_PORTS = new int[] {5443, 5280, 7070};
073
074    private final XmppWebSocketTransport websocketTransport;
075
076    private AbstractWebSocket websocket;
077
078    XmppWebSocketTransportModule(XmppWebSocketTransportModuleDescriptor moduleDescriptor,
079                    ModularXmppClientToServerConnectionInternal connectionInternal) {
080        super(moduleDescriptor, connectionInternal);
081
082        websocketTransport = new XmppWebSocketTransport(connectionInternal);
083    }
084
085    @Override
086    protected XmppWebSocketTransport getTransport() {
087        return websocketTransport;
088    }
089
090    static final class EstablishingWebSocketConnectionStateDescriptor extends StateDescriptor {
091        private EstablishingWebSocketConnectionStateDescriptor() {
092            super(XmppWebSocketTransportModule.EstablishingWebSocketConnectionState.class);
093            addPredeccessor(LookupRemoteConnectionEndpointsStateDescriptor.class);
094            addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class);
095
096            // This states preference to TCP transports over this WebSocket transport implementation.
097            declareInferiorityTo("org.jivesoftware.smack.tcp.XmppTcpTransportModule$EstablishingTcpConnectionStateDescriptor");
098        }
099
100        @Override
101        protected State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) {
102            XmppWebSocketTransportModule websocketTransportModule = connectionInternal.connection.getConnectionModuleFor(
103                            XmppWebSocketTransportModuleDescriptor.class);
104            return websocketTransportModule.constructEstablishingWebSocketConnectionState(this, connectionInternal);
105        }
106    }
107
108    final class EstablishingWebSocketConnectionState extends State.AbstractTransport {
109        EstablishingWebSocketConnectionState(StateDescriptor stateDescriptor,
110                        ModularXmppClientToServerConnectionInternal connectionInternal) {
111            super(websocketTransport, stateDescriptor, connectionInternal);
112        }
113
114        @Override
115        public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws InterruptedException,
116                        NoResponseException, NotConnectedException, SmackException, XMPPException {
117            final WebSocketFactory webSocketFactory;
118            if (moduleDescriptor.webSocketFactory != null) {
119                webSocketFactory = moduleDescriptor.webSocketFactory;
120            } else {
121                webSocketFactory = WebSocketFactoryService::createWebSocket;
122            }
123
124            WebSocketConnectionAttemptState connectionAttemptState = new WebSocketConnectionAttemptState(
125                            connectionInternal, discoveredWebSocketEndpoints, webSocketFactory);
126
127            StateTransitionResult.Failure failure = connectionAttemptState.establishWebSocketConnection();
128            if (failure != null) {
129                return failure;
130            }
131
132            websocket = connectionAttemptState.getConnectedWebSocket();
133
134            connectionInternal.setTransport(websocketTransport);
135
136            // TODO: It appears this should be done in a generic way. I'd assume we always
137            // have to wait for stream features after the connection was established. But I
138            // am not yet 100% positive that this is the case for every transport. Hence, keep it here for now(?).
139            // See also similar comment in XmppTcpTransportModule.
140            // Maybe move this into ConnectedButUnauthenticated state's transitionInto() method? That seems to be the
141            // right place.
142            connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection");
143
144            // Construct a WebSocketConnectedResult using the connected endpoint.
145            return new WebSocketConnectedResult(websocket.getEndpoint());
146        }
147    }
148
149    public EstablishingWebSocketConnectionState constructEstablishingWebSocketConnectionState(
150                    EstablishingWebSocketConnectionStateDescriptor establishingWebSocketConnectionStateDescriptor,
151                    ModularXmppClientToServerConnectionInternal connectionInternal) {
152        return new EstablishingWebSocketConnectionState(establishingWebSocketConnectionStateDescriptor,
153                        connectionInternal);
154    }
155
156    public static final class WebSocketConnectedResult extends StateTransitionResult.Success {
157        final WebSocketRemoteConnectionEndpoint connectedEndpoint;
158
159        public WebSocketConnectedResult(WebSocketRemoteConnectionEndpoint connectedEndpoint) {
160            super("WebSocket connection established with endpoint: " + connectedEndpoint);
161            this.connectedEndpoint = connectedEndpoint;
162        }
163    }
164
165    private DiscoveredWebSocketEndpoints discoveredWebSocketEndpoints;
166
167    /**
168     * Transport class for {@link ModularXmppClientToServerConnectionModule}'s websocket implementation.
169     */
170    public final class XmppWebSocketTransport extends XmppClientToServerTransport {
171
172        AsyncButOrdered<Queue<TopLevelStreamElement>> asyncButOrderedOutgoingElementsQueue;
173
174        XmppWebSocketTransport(ModularXmppClientToServerConnectionInternal connectionInternal) {
175            super(connectionInternal);
176            asyncButOrderedOutgoingElementsQueue = new AsyncButOrdered<Queue<TopLevelStreamElement>>();
177        }
178
179        @Override
180        protected void resetDiscoveredConnectionEndpoints() {
181            discoveredWebSocketEndpoints = null;
182        }
183
184        @Override
185        public boolean hasUseableConnectionEndpoints() {
186            return discoveredWebSocketEndpoints != null;
187        }
188
189        @SuppressWarnings("incomplete-switch")
190        @Override
191        protected List<SmackFuture<LookupConnectionEndpointsResult, Exception>> lookupConnectionEndpoints() {
192            // Assert that there are no stale discovered endpoints prior performing the lookup.
193            assert discoveredWebSocketEndpoints == null;
194
195            InternalSmackFuture<LookupConnectionEndpointsResult, Exception> websocketEndpointsLookupFuture = new InternalSmackFuture<>();
196
197            connectionInternal.asyncGo(() -> {
198                Result result = null;
199
200                ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration();
201                DomainBareJid host = configuration.getXMPPServiceDomain();
202
203                if (moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) {
204                    // Fetch remote endpoints.
205                    result = WebSocketRemoteConnectionEndpointLookup.lookup(host);
206                }
207
208                WebSocketRemoteConnectionEndpoint providedEndpoint = moduleDescriptor.getExplicitlyProvidedEndpoint();
209                if (providedEndpoint != null) {
210                    // If there was not automatic lookup that produced a result, then create a result now.
211                    if (result == null) {
212                        result = new Result();
213                    }
214
215                    // We insert the provided endpoint at the beginning of the list, so that it is used first.
216                    final int INSERT_INDEX = 0;
217                    if (providedEndpoint instanceof SecureWebSocketRemoteConnectionEndpoint) {
218                        SecureWebSocketRemoteConnectionEndpoint secureEndpoint = (SecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
219                        result.discoveredSecureEndpoints.add(INSERT_INDEX, secureEndpoint);
220                    } else if (providedEndpoint instanceof InsecureWebSocketRemoteConnectionEndpoint) {
221                        InsecureWebSocketRemoteConnectionEndpoint insecureEndpoint = (InsecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
222                        result.discoveredInsecureEndpoints.add(INSERT_INDEX, insecureEndpoint);
223                    } else {
224                        throw new AssertionError();
225                    }
226                }
227
228                if (moduleDescriptor.isImplicitWebSocketEndpointEnabled()) {
229                    for (final int securePort : KNOWN_SECURE_PORTS) {
230                        SecureWebSocketRemoteConnectionEndpoint implicitSecureEndpoint = SecureWebSocketRemoteConnectionEndpoint.from(
231                            WebSocketRemoteConnectionEndpoint.SECURE_WEB_SOCKET_SCHEME + "://" + host + ":" + securePort + "/ws");
232                        result.discoveredSecureEndpoints.add(implicitSecureEndpoint);
233                    }
234                    for (final int insecurePort : KNOWN_INSECURE_PORTS) {
235                        InsecureWebSocketRemoteConnectionEndpoint implicitInsecureEndpoint = InsecureWebSocketRemoteConnectionEndpoint.from(
236                            WebSocketRemoteConnectionEndpoint.INSECURE_WEB_SOCKET_SCHEME + "://" + host + ":" + insecurePort + "/ws");
237                        result.discoveredInsecureEndpoints.add(implicitInsecureEndpoint);
238                    }
239                }
240
241                final LookupConnectionEndpointsResult endpointsResult;
242                if (result.isEmpty()) {
243                    endpointsResult = new WebSocketEndpointsDiscoveryFailed(result.lookupFailures);
244                } else {
245                    endpointsResult = new DiscoveredWebSocketEndpoints(result);
246                }
247
248                websocketEndpointsLookupFuture.setResult(endpointsResult);
249            });
250
251            return Collections.singletonList(websocketEndpointsLookupFuture);
252        }
253
254        @Override
255        protected void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess) {
256            discoveredWebSocketEndpoints = (DiscoveredWebSocketEndpoints) lookupConnectionEndpointsSuccess;
257        }
258
259        @Override
260        protected void afterFiltersClosed() {
261        }
262
263        @Override
264        protected void disconnect() {
265            websocket.disconnect(WEBSOCKET_NORMAL_CLOSURE, "WebSocket closed normally");
266        }
267
268        @Override
269        protected void notifyAboutNewOutgoingElements() {
270            final Queue<TopLevelStreamElement> outgoingElementsQueue = connectionInternal.outgoingElementsQueue;
271            asyncButOrderedOutgoingElementsQueue.performAsyncButOrdered(outgoingElementsQueue, () -> {
272                for (TopLevelStreamElement topLevelStreamElement; (topLevelStreamElement = outgoingElementsQueue.poll()) != null;) {
273                    websocket.send(topLevelStreamElement);
274                    connectionInternal.fireFirstLevelElementSendListeners(topLevelStreamElement);
275                }
276            });
277        }
278
279        @Override
280        public SSLSession getSslSession() {
281           return websocket.getSSLSession();
282        }
283
284        @Override
285        public boolean isTransportSecured() {
286            return websocket.isConnectionSecure();
287        }
288
289        @Override
290        public Stats getStats() {
291            return null;
292        }
293
294        @Override
295        public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() {
296            // TODO: Create extra class for this?
297            return new StreamOpenAndCloseFactory() {
298                @Override
299                public AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
300                    return new WebSocketOpenElement(to);
301                }
302                @Override
303                public AbstractStreamClose createStreamClose() {
304                    return new WebSocketCloseElement();
305                }
306            };
307        }
308
309        /**
310         * Contains {@link Result} for successfully discovered endpoints.
311         */
312        public final class DiscoveredWebSocketEndpoints implements LookupConnectionEndpointsSuccess {
313            final WebSocketRemoteConnectionEndpointLookup.Result result;
314
315            DiscoveredWebSocketEndpoints(Result result) {
316                assert result != null;
317                this.result = result;
318            }
319        }
320
321        /**
322         * Contains list of {@link RemoteConnectionEndpointLookupFailure} when no endpoint
323         * could be found during http lookup.
324         */
325        final class WebSocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed {
326            final List<RemoteConnectionEndpointLookupFailure> lookupFailures;
327
328            WebSocketEndpointsDiscoveryFailed(RemoteConnectionEndpointLookupFailure lookupFailure) {
329                this(Collections.singletonList(lookupFailure));
330            }
331
332            WebSocketEndpointsDiscoveryFailed(List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
333                assert lookupFailures != null;
334                this.lookupFailures = Collections.unmodifiableList(lookupFailures);
335            }
336
337            @Override
338            public String toString() {
339                StringBuilder str = new StringBuilder();
340                StringUtils.appendTo(lookupFailures, str);
341                return str.toString();
342            }
343        }
344    }
345}