2020-05-14 14:35:37 +02:00
|
|
|
/**
|
|
|
|
*
|
2021-01-25 19:51:45 +01:00
|
|
|
* Copyright 2020 Aditya Borikar, 2020-2021 Florian Schmaus
|
2020-05-14 14:35:37 +02:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
package org.jivesoftware.smack.websocket;
|
|
|
|
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Queue;
|
|
|
|
|
|
|
|
import javax.net.ssl.SSLSession;
|
|
|
|
|
|
|
|
import org.jivesoftware.smack.AsyncButOrdered;
|
|
|
|
import org.jivesoftware.smack.SmackException;
|
2021-01-25 19:51:45 +01:00
|
|
|
import org.jivesoftware.smack.SmackException.NoResponseException;
|
|
|
|
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
2020-05-14 14:35:37 +02:00
|
|
|
import org.jivesoftware.smack.SmackFuture;
|
|
|
|
import org.jivesoftware.smack.SmackFuture.InternalSmackFuture;
|
|
|
|
import org.jivesoftware.smack.XMPPException;
|
|
|
|
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.ConnectedButUnauthenticatedStateDescriptor;
|
|
|
|
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnection.LookupRemoteConnectionEndpointsStateDescriptor;
|
|
|
|
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionConfiguration;
|
|
|
|
import org.jivesoftware.smack.c2s.ModularXmppClientToServerConnectionModule;
|
|
|
|
import org.jivesoftware.smack.c2s.StreamOpenAndCloseFactory;
|
|
|
|
import org.jivesoftware.smack.c2s.XmppClientToServerTransport;
|
|
|
|
import org.jivesoftware.smack.c2s.internal.ModularXmppClientToServerConnectionInternal;
|
|
|
|
import org.jivesoftware.smack.c2s.internal.WalkStateGraphContext;
|
|
|
|
import org.jivesoftware.smack.fsm.State;
|
|
|
|
import org.jivesoftware.smack.fsm.StateDescriptor;
|
|
|
|
import org.jivesoftware.smack.fsm.StateTransitionResult;
|
|
|
|
import org.jivesoftware.smack.fsm.StateTransitionResult.AttemptResult;
|
|
|
|
import org.jivesoftware.smack.packet.AbstractStreamClose;
|
|
|
|
import org.jivesoftware.smack.packet.AbstractStreamOpen;
|
|
|
|
import org.jivesoftware.smack.packet.TopLevelStreamElement;
|
|
|
|
import org.jivesoftware.smack.util.StringUtils;
|
|
|
|
import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure;
|
2020-09-01 21:30:14 +02:00
|
|
|
import org.jivesoftware.smack.websocket.XmppWebSocketTransportModule.XmppWebSocketTransport.DiscoveredWebSocketEndpoints;
|
|
|
|
import org.jivesoftware.smack.websocket.elements.WebSocketCloseElement;
|
|
|
|
import org.jivesoftware.smack.websocket.elements.WebSocketOpenElement;
|
|
|
|
import org.jivesoftware.smack.websocket.impl.AbstractWebSocket;
|
2021-02-14 19:42:27 +01:00
|
|
|
import org.jivesoftware.smack.websocket.impl.WebSocketFactory;
|
|
|
|
import org.jivesoftware.smack.websocket.impl.WebSocketFactoryService;
|
2021-01-25 19:51:45 +01:00
|
|
|
import org.jivesoftware.smack.websocket.rce.InsecureWebSocketRemoteConnectionEndpoint;
|
|
|
|
import org.jivesoftware.smack.websocket.rce.SecureWebSocketRemoteConnectionEndpoint;
|
2020-09-01 21:30:14 +02:00
|
|
|
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpoint;
|
|
|
|
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup;
|
|
|
|
import org.jivesoftware.smack.websocket.rce.WebSocketRemoteConnectionEndpointLookup.Result;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
|
|
|
import org.jxmpp.jid.DomainBareJid;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The websocket transport module that goes with Smack's modular architecture.
|
|
|
|
*/
|
2020-09-01 21:30:14 +02:00
|
|
|
public final class XmppWebSocketTransportModule
|
|
|
|
extends ModularXmppClientToServerConnectionModule<XmppWebSocketTransportModuleDescriptor> {
|
2021-02-14 19:42:27 +01:00
|
|
|
|
|
|
|
private static final int WEBSOCKET_NORMAL_CLOSURE = 1000;
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
private final XmppWebSocketTransport websocketTransport;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
private AbstractWebSocket websocket;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-28 22:05:47 +01:00
|
|
|
XmppWebSocketTransportModule(XmppWebSocketTransportModuleDescriptor moduleDescriptor,
|
2020-05-14 14:35:37 +02:00
|
|
|
ModularXmppClientToServerConnectionInternal connectionInternal) {
|
|
|
|
super(moduleDescriptor, connectionInternal);
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
websocketTransport = new XmppWebSocketTransport(connectionInternal);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2020-09-01 21:30:14 +02:00
|
|
|
protected XmppWebSocketTransport getTransport() {
|
2020-05-14 14:35:37 +02:00
|
|
|
return websocketTransport;
|
|
|
|
}
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
static final class EstablishingWebSocketConnectionStateDescriptor extends StateDescriptor {
|
|
|
|
private EstablishingWebSocketConnectionStateDescriptor() {
|
|
|
|
super(XmppWebSocketTransportModule.EstablishingWebSocketConnectionState.class);
|
2020-05-14 14:35:37 +02:00
|
|
|
addPredeccessor(LookupRemoteConnectionEndpointsStateDescriptor.class);
|
|
|
|
addSuccessor(ConnectedButUnauthenticatedStateDescriptor.class);
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
// This states preference to TCP transports over this WebSocket transport implementation.
|
2020-05-14 14:35:37 +02:00
|
|
|
declareInferiorityTo("org.jivesoftware.smack.tcp.XmppTcpTransportModule$EstablishingTcpConnectionStateDescriptor");
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected State constructState(ModularXmppClientToServerConnectionInternal connectionInternal) {
|
2020-09-01 21:30:14 +02:00
|
|
|
XmppWebSocketTransportModule websocketTransportModule = connectionInternal.connection.getConnectionModuleFor(
|
|
|
|
XmppWebSocketTransportModuleDescriptor.class);
|
|
|
|
return websocketTransportModule.constructEstablishingWebSocketConnectionState(this, connectionInternal);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
final class EstablishingWebSocketConnectionState extends State.AbstractTransport {
|
2021-01-28 22:05:47 +01:00
|
|
|
EstablishingWebSocketConnectionState(StateDescriptor stateDescriptor,
|
2020-05-14 14:35:37 +02:00
|
|
|
ModularXmppClientToServerConnectionInternal connectionInternal) {
|
2021-01-25 19:51:45 +01:00
|
|
|
super(websocketTransport, stateDescriptor, connectionInternal);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2021-01-25 19:51:45 +01:00
|
|
|
public AttemptResult transitionInto(WalkStateGraphContext walkStateGraphContext) throws InterruptedException,
|
|
|
|
NoResponseException, NotConnectedException, SmackException, XMPPException {
|
2021-02-14 19:42:27 +01:00
|
|
|
final WebSocketFactory webSocketFactory;
|
|
|
|
if (moduleDescriptor.webSocketFactory != null) {
|
|
|
|
webSocketFactory = moduleDescriptor.webSocketFactory;
|
|
|
|
} else {
|
|
|
|
webSocketFactory = WebSocketFactoryService::createWebSocket;
|
|
|
|
}
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
WebSocketConnectionAttemptState connectionAttemptState = new WebSocketConnectionAttemptState(
|
2021-02-14 19:42:27 +01:00
|
|
|
connectionInternal, discoveredWebSocketEndpoints, webSocketFactory);
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
StateTransitionResult.Failure failure = connectionAttemptState.establishWebSocketConnection();
|
|
|
|
if (failure != null) {
|
2020-05-14 14:35:37 +02:00
|
|
|
return failure;
|
|
|
|
}
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
websocket = connectionAttemptState.getConnectedWebSocket();
|
|
|
|
|
2020-05-14 14:35:37 +02:00
|
|
|
connectionInternal.setTransport(websocketTransport);
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
// TODO: It appears this should be done in a generic way. I'd assume we always
|
|
|
|
// have to wait for stream features after the connection was established. But I
|
|
|
|
// am not yet 100% positive that this is the case for every transport. Hence keep it here for now(?).
|
|
|
|
// See also similar comment in XmppTcpTransportModule.
|
|
|
|
// Maybe move this into ConnectedButUnauthenticated state's transitionInto() method? That seems to be the
|
|
|
|
// right place.
|
|
|
|
connectionInternal.newStreamOpenWaitForFeaturesSequence("stream features after initial connection");
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
// Construct a WebSocketConnectedResult using the connected endpoint.
|
2021-01-25 19:51:45 +01:00
|
|
|
return new WebSocketConnectedResult(websocket.getEndpoint());
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
public EstablishingWebSocketConnectionState constructEstablishingWebSocketConnectionState(
|
|
|
|
EstablishingWebSocketConnectionStateDescriptor establishingWebSocketConnectionStateDescriptor,
|
2020-05-14 14:35:37 +02:00
|
|
|
ModularXmppClientToServerConnectionInternal connectionInternal) {
|
2020-09-01 21:30:14 +02:00
|
|
|
return new EstablishingWebSocketConnectionState(establishingWebSocketConnectionStateDescriptor,
|
2020-05-14 14:35:37 +02:00
|
|
|
connectionInternal);
|
|
|
|
}
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
public static final class WebSocketConnectedResult extends StateTransitionResult.Success {
|
|
|
|
final WebSocketRemoteConnectionEndpoint connectedEndpoint;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
public WebSocketConnectedResult(WebSocketRemoteConnectionEndpoint connectedEndpoint) {
|
2021-01-25 19:51:45 +01:00
|
|
|
super("WebSocket connection establised with endpoint: " + connectedEndpoint);
|
2020-05-14 14:35:37 +02:00
|
|
|
this.connectedEndpoint = connectedEndpoint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
private DiscoveredWebSocketEndpoints discoveredWebSocketEndpoints;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Transport class for {@link ModularXmppClientToServerConnectionModule}'s websocket implementation.
|
|
|
|
*/
|
2020-09-01 21:30:14 +02:00
|
|
|
public final class XmppWebSocketTransport extends XmppClientToServerTransport {
|
2020-05-14 14:35:37 +02:00
|
|
|
|
|
|
|
AsyncButOrdered<Queue<TopLevelStreamElement>> asyncButOrderedOutgoingElementsQueue;
|
|
|
|
|
2021-01-28 22:05:47 +01:00
|
|
|
XmppWebSocketTransport(ModularXmppClientToServerConnectionInternal connectionInternal) {
|
2020-05-14 14:35:37 +02:00
|
|
|
super(connectionInternal);
|
|
|
|
asyncButOrderedOutgoingElementsQueue = new AsyncButOrdered<Queue<TopLevelStreamElement>>();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void resetDiscoveredConnectionEndpoints() {
|
2020-09-01 21:30:14 +02:00
|
|
|
discoveredWebSocketEndpoints = null;
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
@Override
|
|
|
|
public boolean hasUseableConnectionEndpoints() {
|
|
|
|
return discoveredWebSocketEndpoints != null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressWarnings("incomplete-switch")
|
2020-05-14 14:35:37 +02:00
|
|
|
@Override
|
|
|
|
protected List<SmackFuture<LookupConnectionEndpointsResult, Exception>> lookupConnectionEndpoints() {
|
|
|
|
// Assert that there are no stale discovered endpoints prior performing the lookup.
|
2020-09-01 21:30:14 +02:00
|
|
|
assert discoveredWebSocketEndpoints == null;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
|
|
|
InternalSmackFuture<LookupConnectionEndpointsResult, Exception> websocketEndpointsLookupFuture = new InternalSmackFuture<>();
|
|
|
|
|
|
|
|
connectionInternal.asyncGo(() -> {
|
2021-01-25 19:51:45 +01:00
|
|
|
Result result = null;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
ModularXmppClientToServerConnectionConfiguration configuration = connectionInternal.connection.getConfiguration();
|
|
|
|
DomainBareJid host = configuration.getXMPPServiceDomain();
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
if (moduleDescriptor.isWebSocketEndpointDiscoveryEnabled()) {
|
|
|
|
// Fetch remote endpoints.
|
|
|
|
result = WebSocketRemoteConnectionEndpointLookup.lookup(host);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
WebSocketRemoteConnectionEndpoint providedEndpoint = moduleDescriptor.getExplicitlyProvidedEndpoint();
|
|
|
|
if (providedEndpoint != null) {
|
|
|
|
// If there was not automatic lookup that produced a result, then create a result now.
|
|
|
|
if (result == null) {
|
|
|
|
result = new Result();
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
// We insert the provided endpoint at the beginning of the list, so that it is used first.
|
|
|
|
final int INSERT_INDEX = 0;
|
|
|
|
if (providedEndpoint instanceof SecureWebSocketRemoteConnectionEndpoint) {
|
|
|
|
SecureWebSocketRemoteConnectionEndpoint secureEndpoint = (SecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
|
|
|
|
result.discoveredSecureEndpoints.add(INSERT_INDEX, secureEndpoint);
|
|
|
|
} else if (providedEndpoint instanceof InsecureWebSocketRemoteConnectionEndpoint) {
|
|
|
|
InsecureWebSocketRemoteConnectionEndpoint insecureEndpoint = (InsecureWebSocketRemoteConnectionEndpoint) providedEndpoint;
|
|
|
|
result.discoveredInsecureEndpoints.add(INSERT_INDEX, insecureEndpoint);
|
|
|
|
} else {
|
|
|
|
throw new AssertionError();
|
|
|
|
}
|
|
|
|
}
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
if (moduleDescriptor.isImplicitWebSocketEndpointEnabled()) {
|
|
|
|
String urlWithoutScheme = "://" + host + ":5443/ws";
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
SecureWebSocketRemoteConnectionEndpoint implicitSecureEndpoint = SecureWebSocketRemoteConnectionEndpoint.from(
|
|
|
|
WebSocketRemoteConnectionEndpoint.SECURE_WEB_SOCKET_SCHEME + urlWithoutScheme);
|
|
|
|
result.discoveredSecureEndpoints.add(implicitSecureEndpoint);
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
InsecureWebSocketRemoteConnectionEndpoint implicitInsecureEndpoint = InsecureWebSocketRemoteConnectionEndpoint.from(
|
|
|
|
WebSocketRemoteConnectionEndpoint.INSECURE_WEB_SOCKET_SCHEME + urlWithoutScheme);
|
|
|
|
result.discoveredInsecureEndpoints.add(implicitInsecureEndpoint);
|
|
|
|
}
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
final LookupConnectionEndpointsResult endpointsResult;
|
|
|
|
if (result.isEmpty()) {
|
|
|
|
endpointsResult = new WebSocketEndpointsDiscoveryFailed(result.lookupFailures);
|
|
|
|
} else {
|
|
|
|
endpointsResult = new DiscoveredWebSocketEndpoints(result);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
2021-01-25 19:51:45 +01:00
|
|
|
|
|
|
|
websocketEndpointsLookupFuture.setResult(endpointsResult);
|
2020-05-14 14:35:37 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return Collections.singletonList(websocketEndpointsLookupFuture);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void loadConnectionEndpoints(LookupConnectionEndpointsSuccess lookupConnectionEndpointsSuccess) {
|
2020-09-01 21:30:14 +02:00
|
|
|
discoveredWebSocketEndpoints = (DiscoveredWebSocketEndpoints) lookupConnectionEndpointsSuccess;
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void afterFiltersClosed() {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void disconnect() {
|
2021-02-14 19:42:27 +01:00
|
|
|
websocket.disconnect(WEBSOCKET_NORMAL_CLOSURE, "WebSocket closed normally");
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void notifyAboutNewOutgoingElements() {
|
2021-01-25 19:51:45 +01:00
|
|
|
final Queue<TopLevelStreamElement> outgoingElementsQueue = connectionInternal.outgoingElementsQueue;
|
2020-05-14 14:35:37 +02:00
|
|
|
asyncButOrderedOutgoingElementsQueue.performAsyncButOrdered(outgoingElementsQueue, () -> {
|
2021-01-25 19:51:45 +01:00
|
|
|
for (TopLevelStreamElement topLevelStreamElement; (topLevelStreamElement = outgoingElementsQueue.poll()) != null;) {
|
|
|
|
websocket.send(topLevelStreamElement);
|
|
|
|
}
|
2020-05-14 14:35:37 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public SSLSession getSslSession() {
|
|
|
|
return websocket.getSSLSession();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isTransportSecured() {
|
|
|
|
return websocket.isConnectionSecure();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Stats getStats() {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public StreamOpenAndCloseFactory getStreamOpenAndCloseFactory() {
|
2021-01-25 19:51:45 +01:00
|
|
|
// TODO: Create extra class for this?
|
2020-05-14 14:35:37 +02:00
|
|
|
return new StreamOpenAndCloseFactory() {
|
|
|
|
@Override
|
2021-01-25 19:51:45 +01:00
|
|
|
public AbstractStreamOpen createStreamOpen(DomainBareJid to, CharSequence from, String id, String lang) {
|
|
|
|
return new WebSocketOpenElement(to);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
@Override
|
|
|
|
public AbstractStreamClose createStreamClose() {
|
2020-09-01 21:30:14 +02:00
|
|
|
return new WebSocketCloseElement();
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Contains {@link Result} for successfully discovered endpoints.
|
|
|
|
*/
|
2020-09-01 21:30:14 +02:00
|
|
|
public final class DiscoveredWebSocketEndpoints implements LookupConnectionEndpointsSuccess {
|
|
|
|
final WebSocketRemoteConnectionEndpointLookup.Result result;
|
2020-05-14 14:35:37 +02:00
|
|
|
|
2020-09-01 21:30:14 +02:00
|
|
|
DiscoveredWebSocketEndpoints(Result result) {
|
2020-05-14 14:35:37 +02:00
|
|
|
assert result != null;
|
|
|
|
this.result = result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Contains list of {@link RemoteConnectionEndpointLookupFailure} when no endpoint
|
|
|
|
* could be found during http lookup.
|
|
|
|
*/
|
2020-09-01 21:30:14 +02:00
|
|
|
final class WebSocketEndpointsDiscoveryFailed implements LookupConnectionEndpointsFailed {
|
2020-05-14 14:35:37 +02:00
|
|
|
final List<RemoteConnectionEndpointLookupFailure> lookupFailures;
|
|
|
|
|
2021-01-25 19:51:45 +01:00
|
|
|
WebSocketEndpointsDiscoveryFailed(RemoteConnectionEndpointLookupFailure lookupFailure) {
|
|
|
|
this(Collections.singletonList(lookupFailure));
|
|
|
|
}
|
|
|
|
|
|
|
|
WebSocketEndpointsDiscoveryFailed(List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
|
|
|
|
assert lookupFailures != null;
|
|
|
|
this.lookupFailures = Collections.unmodifiableList(lookupFailures);
|
2020-05-14 14:35:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
StringBuilder str = new StringBuilder();
|
|
|
|
StringUtils.appendTo(lookupFailures, str);
|
|
|
|
return str.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|