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 establised 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 } 275 }); 276 } 277 278 @Override 279 public SSLSession getSslSession() { 280 return websocket.getSSLSession(); 281 } 282 283 @Override 284 public boolean isTransportSecured() { 285 return websocket.isConnectionSecure(); 286 } 287 288 @Override 289 public Stats getStats() { 290 return null; 291 } 292 293 @Override 294 public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() { 295 // TODO: Create extra class for this? 296 return new StreamOpenAndCloseFactory() { 297 @Override 298 public AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) { 299 return new WebSocketOpenElement(to); 300 } 301 @Override 302 public AbstractStreamClose createStreamClose() { 303 return new WebSocketCloseElement(); 304 } 305 }; 306 } 307 308 /** 309 * Contains {@link Result} for successfully discovered endpoints. 310 */ 311 public final class DiscoveredWebSocketEndpoints implements LookupConnectionEndpointsSuccess { 312 final WebSocketRemoteConnectionEndpointLookup.Result result; 313 314 DiscoveredWebSocketEndpoints(Result result) { 315 assert result != null; 316 this.result = result; 317 } 318 } 319 320 /** 321 * Contains list of {@link RemoteConnectionEndpointLookupFailure} when no endpoint 322 * could be found during http lookup. 323 */ 324 final class WebSocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed { 325 final List<RemoteConnectionEndpointLookupFailure> lookupFailures; 326 327 WebSocketEndpointsDiscoveryFailed(RemoteConnectionEndpointLookupFailure lookupFailure) { 328 this(Collections.singletonList(lookupFailure)); 329 } 330 331 WebSocketEndpointsDiscoveryFailed(List<RemoteConnectionEndpointLookupFailure> lookupFailures) { 332 assert lookupFailures != null; 333 this.lookupFailures = Collections.unmodifiableList(lookupFailures); 334 } 335 336 @Override 337 public String toString() { 338 StringBuilder str = new StringBuilder(); 339 StringUtils.appendTo(lookupFailures, str); 340 return str.toString(); 341 } 342 } 343 } 344}