
io.undertow.websockets.jsr.ServerWebSocketContainer Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 io.undertow.websockets.jsr;
import io.undertow.protocols.ssl.UndertowXnioSsl;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.HttpUpgradeListener;
import io.undertow.servlet.api.ClassIntrospecter;
import io.undertow.servlet.api.InstanceFactory;
import io.undertow.servlet.api.InstanceHandle;
import io.undertow.servlet.api.ThreadSetupHandler;
import io.undertow.servlet.spec.ServletContextImpl;
import io.undertow.servlet.util.ConstructorInstanceFactory;
import io.undertow.servlet.util.ImmediateInstanceHandle;
import io.undertow.servlet.websockets.ServletWebSocketHttpExchange;
import io.undertow.util.CopyOnWriteMap;
import io.undertow.util.PathTemplate;
import io.undertow.util.StatusCodes;
import io.undertow.websockets.WebSocketExtension;
import io.undertow.websockets.client.WebSocketClient;
import io.undertow.websockets.client.WebSocketClientNegotiation;
import io.undertow.websockets.core.WebSocketChannel;
import io.undertow.websockets.core.protocol.Handshake;
import io.undertow.websockets.extensions.ExtensionHandshake;
import io.undertow.websockets.jsr.annotated.AnnotatedEndpointFactory;
import io.undertow.websockets.jsr.handshake.HandshakeUtil;
import io.undertow.websockets.jsr.handshake.JsrHybi07Handshake;
import io.undertow.websockets.jsr.handshake.JsrHybi08Handshake;
import io.undertow.websockets.jsr.handshake.JsrHybi13Handshake;
import org.xnio.IoFuture;
import org.xnio.IoUtils;
import io.undertow.connector.ByteBufferPool;
import org.xnio.OptionMap;
import org.xnio.StreamConnection;
import org.xnio.XnioWorker;
import org.xnio.http.UpgradeFailedException;
import org.xnio.ssl.XnioSsl;
import javax.net.ssl.SSLContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.ClientEndpointConfig;
import jakarta.websocket.CloseReason;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Endpoint;
import jakarta.websocket.Extension;
import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerContainer;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.websocket.server.ServerEndpointConfig;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.channels.ClosedChannelException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static java.lang.System.*;
/**
* {@link ServerContainer} implementation which allows to deploy endpoints for a server.
*
* @author Norman Maurer
*/
public class ServerWebSocketContainer implements ServerContainer, Closeable {
public static final String TIMEOUT = "io.undertow.websocket.CONNECT_TIMEOUT";
public static final int DEFAULT_WEB_SOCKET_TIMEOUT_SECONDS = 10;
private final ClassIntrospecter classIntrospecter;
private final Map, ConfiguredClientEndpoint> clientEndpoints = new CopyOnWriteMap<>();
private final List configuredServerEndpoints = new ArrayList<>();
private final Set> annotatedEndpointClasses = new HashSet<>();
/**
* set of all deployed server endpoint paths. Due to the comparison function we can detect
* overlaps
*/
private final TreeSet seenPaths = new TreeSet<>();
private final Supplier xnioWorker;
private final ByteBufferPool bufferPool;
private final boolean dispatchToWorker;
private final InetSocketAddress clientBindAddress;
private final WebSocketReconnectHandler webSocketReconnectHandler;
private volatile long defaultAsyncSendTimeout;
private volatile long defaultMaxSessionIdleTimeout;
private volatile int defaultMaxBinaryMessageBufferSize;
private volatile int defaultMaxTextMessageBufferSize;
private volatile boolean deploymentComplete = false;
private final List deploymentExceptions = new ArrayList<>();
private ServletContextImpl contextToAddFilter = null;
private final List clientSslProviders;
private final List pauseListeners = new ArrayList<>();
private final List installedExtensions;
private final ThreadSetupHandler.Action invokeEndpointTask;
private volatile boolean closed = false;
public ServerWebSocketContainer(final ClassIntrospecter classIntrospecter, final Supplier xnioWorker, ByteBufferPool bufferPool, List threadSetupHandlers, boolean dispatchToWorker, boolean clientMode) {
this(classIntrospecter, ServerWebSocketContainer.class.getClassLoader(), xnioWorker, bufferPool, threadSetupHandlers, dispatchToWorker, null, null);
}
public ServerWebSocketContainer(final ClassIntrospecter classIntrospecter, final ClassLoader classLoader, Supplier xnioWorker, ByteBufferPool bufferPool, List threadSetupHandlers, boolean dispatchToWorker) {
this(classIntrospecter, classLoader, xnioWorker, bufferPool, threadSetupHandlers, dispatchToWorker, null, null);
}
public ServerWebSocketContainer(final ClassIntrospecter classIntrospecter, final ClassLoader classLoader, Supplier xnioWorker, ByteBufferPool bufferPool, List threadSetupHandlers, boolean dispatchToWorker, InetSocketAddress clientBindAddress, WebSocketReconnectHandler reconnectHandler) {
this(classIntrospecter, classLoader, xnioWorker, bufferPool, threadSetupHandlers, dispatchToWorker, clientBindAddress, reconnectHandler, Collections.emptyList());
}
public ServerWebSocketContainer(final ClassIntrospecter classIntrospecter, final ClassLoader classLoader, Supplier xnioWorker, ByteBufferPool bufferPool, List threadSetupHandlers, boolean dispatchToWorker, InetSocketAddress clientBindAddress, WebSocketReconnectHandler reconnectHandler, List installedExtensions) {
this.classIntrospecter = classIntrospecter;
this.bufferPool = bufferPool;
this.xnioWorker = xnioWorker;
this.dispatchToWorker = dispatchToWorker;
this.clientBindAddress = clientBindAddress;
this.installedExtensions = new ArrayList<>(installedExtensions);
List clientSslProviders = new ArrayList<>();
for (WebsocketClientSslProvider provider : ServiceLoader.load(WebsocketClientSslProvider.class, classLoader)) {
clientSslProviders.add(provider);
}
this.clientSslProviders = Collections.unmodifiableList(clientSslProviders);
this.webSocketReconnectHandler = reconnectHandler;
ThreadSetupHandler.Action task = new ThreadSetupHandler.Action() {
@Override
public Void call(HttpServerExchange exchange, Runnable context) throws Exception {
context.run();
return null;
}
};
for(ThreadSetupHandler handler : threadSetupHandlers) {
task = handler.create(task);
}
this.invokeEndpointTask = task;
}
@Override
public long getDefaultAsyncSendTimeout() {
return defaultAsyncSendTimeout;
}
@Override
public void setAsyncSendTimeout(long defaultAsyncSendTimeout) {
this.defaultAsyncSendTimeout = defaultAsyncSendTimeout;
}
public Session connectToServer(final Object annotatedEndpointInstance, WebSocketClient.ConnectionBuilder connectionBuilder) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
ConfiguredClientEndpoint config = getClientEndpoint(annotatedEndpointInstance.getClass(), false);
if (config == null) {
throw JsrWebSocketMessages.MESSAGES.notAValidClientEndpointType(annotatedEndpointInstance.getClass());
}
Endpoint instance = config.getFactory().createInstance(new ImmediateInstanceHandle<>(annotatedEndpointInstance));
return connectToServerInternal(instance, config, connectionBuilder);
}
@Override
public Session connectToServer(final Object annotatedEndpointInstance, final URI path) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
ConfiguredClientEndpoint config = getClientEndpoint(annotatedEndpointInstance.getClass(), false);
if (config == null) {
throw JsrWebSocketMessages.MESSAGES.notAValidClientEndpointType(annotatedEndpointInstance.getClass());
}
Endpoint instance = config.getFactory().createInstance(new ImmediateInstanceHandle<>(annotatedEndpointInstance));
XnioSsl ssl = null;
for (WebsocketClientSslProvider provider : clientSslProviders) {
ssl = provider.getSsl(xnioWorker.get(), annotatedEndpointInstance, path);
if (ssl != null) {
break;
}
}
if(ssl == null) {
try {
ssl = new UndertowXnioSsl(xnioWorker.get().getXnio(), OptionMap.EMPTY, SSLContext.getDefault());
} catch (NoSuchAlgorithmException e) {
//ignore
}
}
return connectToServerInternal(instance, ssl, config, path);
}
public Session connectToServer(Class> aClass, WebSocketClient.ConnectionBuilder connectionBuilder) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
ConfiguredClientEndpoint config = getClientEndpoint(aClass, true);
if (config == null) {
throw JsrWebSocketMessages.MESSAGES.notAValidClientEndpointType(aClass);
}
try {
AnnotatedEndpointFactory factory = config.getFactory();
InstanceHandle> instance = config.getInstanceFactory().createInstance();
return connectToServerInternal(factory.createInstance(instance), config, connectionBuilder);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
@Override
public Session connectToServer(Class> aClass, URI uri) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
ConfiguredClientEndpoint config = getClientEndpoint(aClass, true);
if (config == null) {
throw JsrWebSocketMessages.MESSAGES.notAValidClientEndpointType(aClass);
}
try {
AnnotatedEndpointFactory factory = config.getFactory();
InstanceHandle> instance = config.getInstanceFactory().createInstance();
XnioSsl ssl = null;
for (WebsocketClientSslProvider provider : clientSslProviders) {
ssl = provider.getSsl(xnioWorker.get(), aClass, uri);
if (ssl != null) {
break;
}
}
if(ssl == null) {
try {
ssl = new UndertowXnioSsl(xnioWorker.get().getXnio(), OptionMap.EMPTY, SSLContext.getDefault());
} catch (NoSuchAlgorithmException e) {
//ignore
}
}
return connectToServerInternal(factory.createInstance(instance), ssl, config, uri);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
@Override
public Session connectToServer(final Endpoint endpointInstance, final ClientEndpointConfig config, final URI path) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
ClientEndpointConfig cec = config != null ? config : ClientEndpointConfig.Builder.create().build();
XnioSsl ssl = null;
for (WebsocketClientSslProvider provider : clientSslProviders) {
ssl = provider.getSsl(xnioWorker.get(), endpointInstance, cec, path);
if (ssl != null) {
break;
}
}
if(ssl == null) {
try {
ssl = new UndertowXnioSsl(xnioWorker.get().getXnio(), OptionMap.EMPTY, SSLContext.getDefault());
} catch (NoSuchAlgorithmException e) {
//ignore
}
}
//in theory we should not be able to connect until the deployment is complete, but the definition of when a deployment is complete is a bit nebulous.
WebSocketClientNegotiation clientNegotiation = new ClientNegotiation(cec.getPreferredSubprotocols(), toExtensionList(cec.getExtensions()), cec);
WebSocketClient.ConnectionBuilder connectionBuilder = WebSocketClient.connectionBuilder(xnioWorker.get(), bufferPool, path)
.setSsl(ssl)
.setBindAddress(clientBindAddress)
.setClientNegotiation(clientNegotiation);
return connectToServer(endpointInstance, config, connectionBuilder);
}
public Session connectToServer(final Endpoint endpointInstance, final ClientEndpointConfig config, WebSocketClient.ConnectionBuilder connectionBuilder) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
ClientEndpointConfig cec = config != null ? config : ClientEndpointConfig.Builder.create().build();
WebSocketClientNegotiation clientNegotiation = connectionBuilder.getClientNegotiation();
IoFuture session = connectionBuilder
.connect();
Number timeout = (Number) cec.getUserProperties().get(TIMEOUT);
if(session.await(timeout == null ? DEFAULT_WEB_SOCKET_TIMEOUT_SECONDS: timeout.intValue(), TimeUnit.SECONDS) == IoFuture.Status.WAITING) {
//add a notifier to close the channel if the connection actually completes
session.cancel();
session.addNotifier(new IoFuture.HandlingNotifier() {
@Override
public void handleDone(WebSocketChannel data, Object attachment) {
IoUtils.safeClose(data);
}
}, null);
throw JsrWebSocketMessages.MESSAGES.connectionTimedOut();
}
WebSocketChannel channel;
try {
channel = session.get();
} catch (UpgradeFailedException e) {
throw new DeploymentException(e.getMessage(), e);
}
EndpointSessionHandler sessionHandler = new EndpointSessionHandler(this);
final List extensions = new ArrayList<>();
final Map extMap = new HashMap<>();
for (Extension ext : cec.getExtensions()) {
extMap.put(ext.getName(), ext);
}
for (WebSocketExtension e : clientNegotiation.getSelectedExtensions()) {
Extension ext = extMap.get(e.getName());
if (ext == null) {
throw JsrWebSocketMessages.MESSAGES.extensionWasNotPresentInClientHandshake(e.getName(), clientNegotiation.getSupportedExtensions());
}
extensions.add(ExtensionImpl.create(e));
}
ConfiguredClientEndpoint configured = clientEndpoints.get(endpointInstance.getClass());
Endpoint instance = endpointInstance;
if(configured == null) {
synchronized (clientEndpoints) {
// make sure to create an instance of AnnotatedEndpoint if we have the annotation
configured = getClientEndpoint(endpointInstance.getClass(), false);
if(configured == null) {
// if we don't, add an endpoint anyway to the list of clientEndpoints
clientEndpoints.put(endpointInstance.getClass(), configured = new ConfiguredClientEndpoint());
} else {
// use the factory in configured to reach the endpoint
instance = configured.getFactory().createInstance(new ImmediateInstanceHandle<>(endpointInstance));
}
}
}
EncodingFactory encodingFactory = EncodingFactory.createFactory(classIntrospecter, cec.getDecoders(), cec.getEncoders());
UndertowSession undertowSession = new UndertowSession(channel, connectionBuilder.getUri(), Collections.emptyMap(), Collections.>emptyMap(), sessionHandler, null, new ImmediateInstanceHandle<>(endpointInstance), cec, connectionBuilder.getUri().getQuery(), encodingFactory.createEncoding(cec), configured, clientNegotiation.getSelectedSubProtocol(), extensions, connectionBuilder);
instance.onOpen(undertowSession, cec);
channel.resumeReceives();
return undertowSession;
}
@Override
public Session connectToServer(final Class extends Endpoint> endpointClass, final ClientEndpointConfig cec, final URI path) throws DeploymentException, IOException {
if(closed) {
throw new ClosedChannelException();
}
try {
Endpoint endpoint = classIntrospecter.createInstanceFactory(endpointClass).createInstance().getInstance();
return connectToServer(endpoint, cec, path);
} catch (InstantiationException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
@Override
public void upgradeHttpToWebSocket(Object req, Object res, ServerEndpointConfig sec, Map pathParameters) throws IOException, DeploymentException {
try {
doUpgrade((HttpServletRequest) req, (HttpServletResponse) res, sec, pathParameters);
} catch (final ServletException e) {
throw new DeploymentException(e.getRootCause().getMessage(), e.getRootCause());
}
}
public void doUpgrade(HttpServletRequest request,
HttpServletResponse response, final ServerEndpointConfig sec,
Map pathParams)
throws ServletException, IOException {
ServerEndpointConfig.Configurator configurator = sec.getConfigurator();
try {
EncodingFactory encodingFactory = EncodingFactory.createFactory(classIntrospecter, sec.getDecoders(), sec.getEncoders());
PathTemplate pt = PathTemplate.create(sec.getPath());
InstanceFactory> instanceFactory = null;
try {
instanceFactory = classIntrospecter.createInstanceFactory(sec.getEndpointClass());
} catch (Exception e) {
//so it is possible that this is still valid if a custom configurator is in use
if (configurator == null || configurator.getClass() == ServerEndpointConfig.Configurator.class) {
throw JsrWebSocketMessages.MESSAGES.couldNotDeploy(e);
} else {
instanceFactory = new InstanceFactory
© 2015 - 2025 Weber Informatics LLC | Privacy Policy