001/**
002 *
003 * Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.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 final XmppWebSocketTransport websocketTransport;
072
073    private AbstractWebSocket websocket;
074
075    XmppWebSocketTransportModule(XmppWebSocketTransportModuleDescriptor moduleDescriptor,
076                    ModularXmppClientToServerConnectionInternal connectionInternal) {
077        super(moduleDescriptor, connectionInternal);
078
079        websocketTransport = new XmppWebSocketTransport(connectionInternal);
080    }
081
082    @Override
083    protected XmppWebSocketTransport getTransport() {
084        return websocketTransport;
085    }
086
087    static final class EstablishingWebSocketConnectionStateDescriptor extends StateDescriptor {
088        private EstablishingWebSocketConnectionStateDescriptor() {
089            super(XmppWebSocketTransportModule.EstablishingWebSocketConnectionState.class);
090            addPredeccessor(LookupRemoteConnectionEndpointsStateDescriptor.class);
091            addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class);
092
093            // This states preference to TCP transports over this WebSocket transport implementation.
094            declareInferiorityTo("org.jivesoftware.smack.tcp.XmppTcpTransportModule$EstablishingTcpConnectionStateDescriptor");
095        }
096
097        @Override
098        protected State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) {
099            XmppWebSocketTransportModule websocketTransportModule = connectionInternal.connection.getConnectionModuleFor(
100                            XmppWebSocketTransportModuleDescriptor.class);
101            return websocketTransportModule.constructEstablishingWebSocketConnectionState(this, connectionInternal);
102        }
103    }
104
105    final class EstablishingWebSocketConnectionState extends State.AbstractTransport {
106        EstablishingWebSocketConnectionState(StateDescriptor stateDescriptor,
107                        ModularXmppClientToServerConnectionInternal connectionInternal) {
108            super(websocketTransport, stateDescriptor, connectionInternal);
109        }
110
111        @Override
112        public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws InterruptedException,
113                        NoResponseException, NotConnectedException, SmackException, XMPPException {
114            final WebSocketFactory webSocketFactory;
115            if (moduleDescriptor.webSocketFactory != null) {
116                webSocketFactory = moduleDescriptor.webSocketFactory;
117            } else {
118                webSocketFactory = WebSocketFactoryService::createWebSocket;
119            }
120
121            WebSocketConnectionAttemptState connectionAttemptState = new WebSocketConnectionAttemptState(
122                            connectionInternal, discoveredWebSocketEndpoints, webSocketFactory);
123
124            StateTransitionResult.Failure failure = connectionAttemptState.establishWebSocketConnection();
125            if (failure != null) {
126                return failure;
127            }
128
129            websocket = connectionAttemptState.getConnectedWebSocket();
130
131            connectionInternal.setTransport(websocketTransport);
132
133            // TODO: It appears this should be done in a generic way. I'd assume we always
134            // have to wait for stream features after the connection was established. But I
135            // am not yet 100% positive that this is the case for every transport. Hence keep it here for now(?).
136            // See also similar comment in XmppTcpTransportModule.
137            // Maybe move this into ConnectedButUnauthenticated state's transitionInto() method? That seems to be the
138            // right place.
139            connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection");
140
141            // Construct a WebSocketConnectedResult using the connected endpoint.
142            return new WebSocketConnectedResult(websocket.getEndpoint());
143        }
144    }
145
146    public EstablishingWebSocketConnectionState constructEstablishingWebSocketConnectionState(
147                    EstablishingWebSocketConnectionStateDescriptor establishingWebSocketConnectionStateDescriptor,
148                    ModularXmppClientToServerConnectionInternal connectionInternal) {
149        return new EstablishingWebSocketConnectionState(establishingWebSocketConnectionStateDescriptor,
150                        connectionInternal);
151    }
152
153    public static final class WebSocketConnectedResult extends StateTransitionResult.Success {
154        final WebSocketRemoteConnectionEndpoint connectedEndpoint;
155
156        public WebSocketConnectedResult(WebSocketRemoteConnectionEndpoint connectedEndpoint) {
157            super("WebSocket connection establised with endpoint: " + connectedEndpoint);
158            this.connectedEndpoint = connectedEndpoint;
159        }
160    }
161
162    private DiscoveredWebSocketEndpoints discoveredWebSocketEndpoints;
163
164    /**
165     * Transport class for {@link ModularXmppClientToServerConnectionModule}'s websocket implementation.
166     */
167    public final class XmppWebSocketTransport extends XmppClientToServerTransport {
168
169        AsyncButOrdered<Queue<TopLevelStreamElement>> asyncButOrderedOutgoingElementsQueue;
170
171        XmppWebSocketTransport(ModularXmppClientToServerConnectionInternal connectionInternal) {
172            super(connectionInternal);
173            asyncButOrderedOutgoingElementsQueue = new AsyncButOrdered<Queue<TopLevelStreamElement>>();
174        }
175
176        @Override
177        protected void resetDiscoveredConnectionEndpoints() {
178            discoveredWebSocketEndpoints = null;
179        }
180
181        @Override
182        public boolean hasUseableConnectionEndpoints() {
183            return discoveredWebSocketEndpoints != null;
184        }
185
186        @SuppressWarnings("incomplete-switch")
187        @Override
188        protected List<SmackFuture<LookupConnectionEndpointsResult, Exception>> lookupConnectionEndpoints() {
189            // Assert that there are no stale discovered endpoints prior performing the lookup.
190            assert discoveredWebSocketEndpoints == null;
191
192            InternalSmackFuture<LookupConnectionEndpointsResult, Exception> websocketEndpointsLookupFuture = new InternalSmackFuture<>();
193
194            connectionInternal.asyncGo(() -> {
195                Result result = null;
196
197                ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration();
198                DomainBareJid host = configuration.getXMPPServiceDomain();
199
200                if (moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) {
201                    // Fetch remote endpoints.
202                    result = WebSocketRemoteConnectionEndpointLookup.lookup(host);
203                }
204
205                WebSocketRemoteConnectionEndpoint providedEndpoint = moduleDescriptor.getExplicitlyProvidedEndpoint();
206                if (providedEndpoint != null) {
207                    // If there was not automatic lookup that produced a result, then create a result now.
208                    if (result == null) {
209                        result = new Result();
210                    }
211
212                    // We insert the provided endpoint at the beginning of the list, so that it is used first.
213                    final int INSERT_INDEX = 0;
214                    if (providedEndpoint instanceof SecureWebSocketRemoteConnectionEndpoint) {
215                        SecureWebSocketRemoteConnectionEndpoint secureEndpoint = (SecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
216                        result.discoveredSecureEndpoints.add(INSERT_INDEX, secureEndpoint);
217                    } else if (providedEndpoint instanceof InsecureWebSocketRemoteConnectionEndpoint) {
218                        InsecureWebSocketRemoteConnectionEndpoint insecureEndpoint = (InsecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
219                        result.discoveredInsecureEndpoints.add(INSERT_INDEX, insecureEndpoint);
220                    } else {
221                        throw new AssertionError();
222                    }
223                }
224
225                if (moduleDescriptor.isImplicitWebSocketEndpointEnabled()) {
226                    String urlWithoutScheme = "://" + host + ":5443/ws";
227
228                    SecureWebSocketRemoteConnectionEndpoint implicitSecureEndpoint = SecureWebSocketRemoteConnectionEndpoint.from(
229                                    WebSocketRemoteConnectionEndpoint.SECURE_WEB_SOCKET_SCHEME + urlWithoutScheme);
230                    result.discoveredSecureEndpoints.add(implicitSecureEndpoint);
231
232                    InsecureWebSocketRemoteConnectionEndpoint implicitInsecureEndpoint = InsecureWebSocketRemoteConnectionEndpoint.from(
233                                    WebSocketRemoteConnectionEndpoint.INSECURE_WEB_SOCKET_SCHEME + urlWithoutScheme);
234                    result.discoveredInsecureEndpoints.add(implicitInsecureEndpoint);
235                }
236
237                final LookupConnectionEndpointsResult endpointsResult;
238                if (result.isEmpty()) {
239                    endpointsResult = new WebSocketEndpointsDiscoveryFailed(result.lookupFailures);
240                } else {
241                    endpointsResult = new DiscoveredWebSocketEndpoints(result);
242                }
243
244                websocketEndpointsLookupFuture.setResult(endpointsResult);
245            });
246
247            return Collections.singletonList(websocketEndpointsLookupFuture);
248        }
249
250        @Override
251        protected void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess) {
252            discoveredWebSocketEndpoints = (DiscoveredWebSocketEndpoints) lookupConnectionEndpointsSuccess;
253        }
254
255        @Override
256        protected void afterFiltersClosed() {
257        }
258
259        @Override
260        protected void disconnect() {
261            websocket.disconnect(WEBSOCKET_NORMAL_CLOSURE, "WebSocket closed normally");
262        }
263
264        @Override
265        protected void notifyAboutNewOutgoingElements() {
266            final Queue<TopLevelStreamElement> outgoingElementsQueue = connectionInternal.outgoingElementsQueue;
267            asyncButOrderedOutgoingElementsQueue.performAsyncButOrdered(outgoingElementsQueue, () -> {
268                for (TopLevelStreamElement topLevelStreamElement; (topLevelStreamElement = outgoingElementsQueue.poll()) != null;) {
269                    websocket.send(topLevelStreamElement);
270                }
271            });
272        }
273
274        @Override
275        public SSLSession getSslSession() {
276           return websocket.getSSLSession();
277        }
278
279        @Override
280        public boolean isTransportSecured() {
281            return websocket.isConnectionSecure();
282        }
283
284        @Override
285        public Stats getStats() {
286            return null;
287        }
288
289        @Override
290        public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() {
291            // TODO: Create extra class for this?
292            return new StreamOpenAndCloseFactory() {
293                @Override
294                public AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
295                    return new WebSocketOpenElement(to);
296                }
297                @Override
298                public AbstractStreamClose createStreamClose() {
299                    return new WebSocketCloseElement();
300                }
301            };
302        }
303
304        /**
305         * Contains {@link Result} for successfully discovered endpoints.
306         */
307        public final class DiscoveredWebSocketEndpoints implements LookupConnectionEndpointsSuccess {
308            final WebSocketRemoteConnectionEndpointLookup.Result result;
309
310            DiscoveredWebSocketEndpoints(Result result) {
311                assert result != null;
312                this.result = result;
313            }
314        }
315
316        /**
317         * Contains list of {@link RemoteConnectionEndpointLookupFailure} when no endpoint
318         * could be found during http lookup.
319         */
320        final class WebSocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed {
321            final List<RemoteConnectionEndpointLookupFailure> lookupFailures;
322
323            WebSocketEndpointsDiscoveryFailed(RemoteConnectionEndpointLookupFailure lookupFailure) {
324                this(Collections.singletonList(lookupFailure));
325            }
326
327            WebSocketEndpointsDiscoveryFailed(List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
328                assert lookupFailures != null;
329                this.lookupFailures = Collections.unmodifiableList(lookupFailures);
330            }
331
332            @Override
333            public String toString() {
334                StringBuilder str = new StringBuilder();
335                StringUtils.appendTo(lookupFailures, str);
336                return str.toString();
337            }
338        }
339    }
340}