Skip to content

Commit

Permalink
Merge pull request #5874 from eclipse/jetty-10.0.x-5866-WebSocketProg…
Browse files Browse the repository at this point in the history
…rammaticUpgrade

Issue #5866 - allow WebSocket programmatic upgrades
  • Loading branch information
lachlan-roberts committed Jan 20, 2021
2 parents c8beacb + 0d48b8b commit f2cde85
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@
// ========================================================================
//

package org.eclipse.jetty.websocket.core.server.internal;
package org.eclipse.jetty.websocket.core.server;

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator;
import org.eclipse.jetty.websocket.core.server.internal.HandshakerSelector;

public interface Handshaker
{
static Handshaker newInstance()
{
return new HandshakerSelector();
}

boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest request, HttpServletResponse response, WebSocketComponents components, Configuration.Customizer defaultCustomizer) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.exception.WebSocketException;
import org.eclipse.jetty.websocket.core.server.internal.CreatorNegotiator;
import org.eclipse.jetty.websocket.core.server.internal.Handshaker;
import org.eclipse.jetty.websocket.core.server.internal.HandshakerSelector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -124,6 +123,11 @@ public WebSocketMappings(WebSocketComponents components)
this.components = components;
}

public Handshaker getHandshaker()
{
return handshaker;
}

@Override
public void lifeCycleStopping(LifeCycle context)
{
Expand Down Expand Up @@ -176,7 +180,7 @@ public WebSocketCreator getWebSocketCreator(PathSpec pathSpec)
*/
public void addMapping(PathSpec pathSpec, WebSocketCreator creator, FrameHandlerFactory factory, Configuration.Customizer customizer) throws WebSocketException
{
mappings.put(pathSpec, new CreatorNegotiator(creator, factory, customizer));
mappings.put(pathSpec, WebSocketNegotiator.from(creator, factory, customizer));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@

import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.core.server.internal.CreatorNegotiator;

public interface WebSocketNegotiator extends Configuration.Customizer
{
FrameHandler negotiate(WebSocketNegotiation negotiation) throws IOException;

@Override
default void customize(Configuration configurable)
{
}

static WebSocketNegotiator from(Function<WebSocketNegotiation, FrameHandler> negotiate)
{
return from(negotiate, null);
Expand All @@ -40,6 +46,16 @@ public FrameHandler negotiate(WebSocketNegotiation negotiation)
};
}

static WebSocketNegotiator from(WebSocketCreator creator, FrameHandlerFactory factory)
{
return from(creator, factory, null);
}

static WebSocketNegotiator from(WebSocketCreator creator, FrameHandlerFactory factory, Configuration.Customizer customizer)
{
return new CreatorNegotiator(creator, factory, customizer);
}

abstract class AbstractNegotiator extends Configuration.ConfigurationCustomizer implements WebSocketNegotiator
{
final Configuration.Customizer customizer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.eclipse.jetty.websocket.core.internal.Negotiated;
import org.eclipse.jetty.websocket.core.internal.WebSocketConnection;
import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession;
import org.eclipse.jetty.websocket.core.server.Handshaker;
import org.eclipse.jetty.websocket.core.server.WebSocketNegotiation;
import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator;
import org.slf4j.Logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public class CreatorNegotiator extends WebSocketNegotiator.AbstractNegotiator
private final WebSocketCreator creator;
private final FrameHandlerFactory factory;

public CreatorNegotiator(WebSocketCreator creator, FrameHandlerFactory factory)
{
this(creator, factory, null);
}

public CreatorNegotiator(WebSocketCreator creator, FrameHandlerFactory factory, Customizer customizer)
{
super(customizer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.server.Handshaker;
import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@

package org.eclipse.jetty.websocket.server;

import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.servlet.ServletContextHandler;
Expand All @@ -32,10 +35,14 @@
import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
import org.eclipse.jetty.websocket.common.SessionTracker;
import org.eclipse.jetty.websocket.core.Configuration;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.exception.WebSocketException;
import org.eclipse.jetty.websocket.core.internal.util.ReflectUtils;
import org.eclipse.jetty.websocket.core.server.FrameHandlerFactory;
import org.eclipse.jetty.websocket.core.server.Handshaker;
import org.eclipse.jetty.websocket.core.server.WebSocketCreator;
import org.eclipse.jetty.websocket.core.server.WebSocketMappings;
import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.server.internal.DelegatedServerUpgradeRequest;
import org.eclipse.jetty.websocket.server.internal.DelegatedServerUpgradeResponse;
Expand Down Expand Up @@ -69,7 +76,8 @@ public static JettyWebSocketServerContainer ensureContainer(ServletContext servl

// Create the Jetty ServerContainer implementation
WebSocketMappings mappings = WebSocketMappings.ensureMappings(servletContext);
container = new JettyWebSocketServerContainer(contextHandler, mappings, executor);
WebSocketComponents components = WebSocketServerComponents.getWebSocketComponents(servletContext);
container = new JettyWebSocketServerContainer(contextHandler, mappings, components, executor);
servletContext.setAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE, container);
contextHandler.addManaged(container);
contextHandler.addEventListener(container);
Expand All @@ -82,7 +90,8 @@ public static JettyWebSocketServerContainer ensureContainer(ServletContext servl

private final ServletContextHandler contextHandler;
private final WebSocketMappings webSocketMappings;
private final FrameHandlerFactory frameHandlerFactory;
private final WebSocketComponents components;
private final JettyServerFrameHandlerFactory frameHandlerFactory;
private final Executor executor;
private final Configuration.ConfigurationCustomizer customizer = new Configuration.ConfigurationCustomizer();

Expand All @@ -95,21 +104,14 @@ public static JettyWebSocketServerContainer ensureContainer(ServletContext servl
* @param webSocketMappings the {@link WebSocketMappings} that this container belongs to
* @param executor the {@link Executor} to use
*/
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMappings webSocketMappings, Executor executor)
JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMappings webSocketMappings, WebSocketComponents components, Executor executor)
{
this.contextHandler = contextHandler;
this.webSocketMappings = webSocketMappings;
this.components = components;
this.executor = executor;

// Ensure there is a FrameHandlerFactory
JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class);
if (factory == null)
{
factory = new JettyServerFrameHandlerFactory(this);
contextHandler.addManaged(factory);
contextHandler.addEventListener(factory);
}
frameHandlerFactory = factory;
this.frameHandlerFactory = new JettyServerFrameHandlerFactory(this);
addBean(frameHandlerFactory);

addSessionListener(sessionTracker);
addBean(sessionTracker);
Expand Down Expand Up @@ -145,6 +147,23 @@ public void addMapping(String pathSpec, final Class<?> endpointClass)
});
}

/**
* An immediate programmatic WebSocket upgrade that does not register a mapping or create a {@link WebSocketUpgradeFilter}.
* @param creator the WebSocketCreator to use.
* @param request the HttpServletRequest.
* @param response the HttpServletResponse.
* @return true if the connection was successfully upgraded to WebSocket.
* @throws IOException if an I/O error occurs.
*/
public boolean upgrade(JettyWebSocketCreator creator, HttpServletRequest request, HttpServletResponse response) throws IOException
{
WebSocketCreator coreCreator = (req, resp) -> creator.createWebSocket(new DelegatedServerUpgradeRequest(req), new DelegatedServerUpgradeResponse(resp));
WebSocketNegotiator negotiator = WebSocketNegotiator.from(coreCreator, frameHandlerFactory, customizer);

Handshaker handshaker = webSocketMappings.getHandshaker();
return handshaker.upgradeRequest(negotiator, request, response, components, null);
}

@Override
public Executor getExecutor()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ public abstract class JettyWebSocketServlet extends HttpServlet
private FrameHandlerFactory getFactory()
{
JettyServerFrameHandlerFactory frameHandlerFactory = JettyServerFrameHandlerFactory.getFactory(getServletContext());

if (frameHandlerFactory == null)
throw new IllegalStateException("JettyServerFrameHandlerFactory not found");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,21 @@

import javax.servlet.ServletContext;

import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.websocket.api.WebSocketContainer;
import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler;
import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandlerFactory;
import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.core.server.FrameHandlerFactory;
import org.eclipse.jetty.websocket.core.server.ServerUpgradeRequest;
import org.eclipse.jetty.websocket.core.server.ServerUpgradeResponse;
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;

public class JettyServerFrameHandlerFactory extends JettyWebSocketFrameHandlerFactory implements FrameHandlerFactory, LifeCycle.Listener
public class JettyServerFrameHandlerFactory extends JettyWebSocketFrameHandlerFactory implements FrameHandlerFactory
{
public static JettyServerFrameHandlerFactory getFactory(ServletContext context)
public static JettyServerFrameHandlerFactory getFactory(ServletContext servletContext)
{
ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context, "JettyServerFrameHandlerFactory");
return contextHandler.getBean(JettyServerFrameHandlerFactory.class);
JettyWebSocketServerContainer container = JettyWebSocketServerContainer.getContainer(servletContext);
return (container == null) ? null : container.getBean(JettyServerFrameHandlerFactory.class);
}

public JettyServerFrameHandlerFactory(WebSocketContainer container)
Expand All @@ -47,16 +45,4 @@ public FrameHandler newFrameHandler(Object websocketPojo, ServerUpgradeRequest u
frameHandler.setUpgradeResponse(new DelegatedServerUpgradeResponse(upgradeResponse));
return frameHandler;
}

@Override
public void lifeCycleStopping(LifeCycle context)
{
ContextHandler contextHandler = (ContextHandler)context;
JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class);
if (factory != null)
{
contextHandler.removeBean(factory);
LifeCycle.stop(factory);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.websocket.tests;

import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ProgrammaticWebSocketUpgradeTest
{
private Server server;
private ServerConnector connector;
private WebSocketClient client;
private ServletContextHandler contextHandler;

@BeforeEach
public void before() throws Exception
{
client = new WebSocketClient();
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);

contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
contextHandler.addServlet(new ServletHolder(new CustomUpgradeServlet()), "/");
server.setHandler(contextHandler);

JettyWebSocketServletContainerInitializer.configure(contextHandler, null);

server.start();
client.start();
}

@AfterEach
public void stop() throws Exception
{
client.stop();
server.stop();
}

public static class CustomUpgradeServlet extends HttpServlet
{
private JettyWebSocketServerContainer container;

@Override
public void init(ServletConfig config) throws ServletException
{
super.init(config);
container = JettyWebSocketServerContainer.getContainer(getServletContext());
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
JettyWebSocketCreator creator = (req, resp) -> new EchoSocket();
container.upgrade(creator, request, response);
}
}

@Test
public void testProgrammaticWebSocketUpgrade() throws Exception
{
// Test we can upgrade to websocket and send a message.
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/path");
EventSocket socket = new EventSocket();
CompletableFuture<Session> connect = client.connect(socket, uri);
try (Session session = connect.get(5, TimeUnit.SECONDS))
{
session.getRemote().sendString("hello world");
}
assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS));

String msg = socket.textMessages.poll();
assertThat(msg, is("hello world"));

// WebSocketUpgradeFilter should not have been added.
assertThat(contextHandler.getServletHandler().getFilters().length, is(0));
}
}

0 comments on commit f2cde85

Please sign in to comment.