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}