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