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}