diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/JettyBackedGrpcServer.java b/server/jetty/src/main/java/io/deephaven/server/jetty/JettyBackedGrpcServer.java index 19acd0acc55..8ba0eb5abc0 100644 --- a/server/jetty/src/main/java/io/deephaven/server/jetty/JettyBackedGrpcServer.java +++ b/server/jetty/src/main/java/io/deephaven/server/jetty/JettyBackedGrpcServer.java @@ -29,6 +29,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.util.resource.Resource; @@ -49,7 +51,7 @@ import static io.grpc.servlet.web.websocket.MultiplexedWebSocketServerStream.GRPC_WEBSOCKETS_MULTIPLEX_PROTOCOL; import static io.grpc.servlet.web.websocket.WebSocketServerStream.GRPC_WEBSOCKETS_PROTOCOL; -import static org.eclipse.jetty.servlet.ServletContextHandler.SESSIONS; +import static org.eclipse.jetty.servlet.ServletContextHandler.NO_SESSIONS; public class JettyBackedGrpcServer implements GrpcServer { @@ -63,7 +65,7 @@ public JettyBackedGrpcServer( jetty.addConnector(createConnector(jetty, config)); final WebAppContext context = - new WebAppContext(null, "/", null, null, null, new ErrorPageErrorHandler(), SESSIONS); + new WebAppContext(null, "/", null, null, null, new ErrorPageErrorHandler(), NO_SESSIONS); try { String knownFile = "/ide/index.html"; URL ide = JettyBackedGrpcServer.class.getResource(knownFile); @@ -72,6 +74,7 @@ public JettyBackedGrpcServer( } catch (IOException ioException) { throw new UncheckedIOException(ioException); } + context.setInitParameter(DefaultServlet.CONTEXT_INIT + "dirAllowed", "false"); // For the Web UI, cache everything in the static folder // https://create-react-app.dev/docs/production-build/#static-file-caching @@ -85,9 +88,6 @@ public JettyBackedGrpcServer( // Add an extra filter to redirect from / to /ide/ context.addFilter(HomeFilter.class, "/", EnumSet.noneOf(DispatcherType.class)); - // Direct jetty all use this configuration as the root application - context.setContextPath("/"); - // Handle grpc-web connections, translate to vanilla grpc context.addFilter(new FilterHolder(new GrpcWebFilter()), "/*", EnumSet.noneOf(DispatcherType.class)); @@ -123,7 +123,14 @@ public T getEndpointInstance(Class endpointClass) throws InstantiationExc ); }); } - jetty.setHandler(context); + + // Note: handler order matters due to pathSpec order + HandlerCollection handlers = new HandlerCollection(); + // Set up /js-plugins/* + JsPlugins.maybeAdd(handlers::addHandler); + // Set up /* + handlers.addHandler(context); + jetty.setHandler(handlers); } @Override diff --git a/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java new file mode 100644 index 00000000000..f2ec9b2fe77 --- /dev/null +++ b/server/jetty/src/main/java/io/deephaven/server/jetty/JsPlugins.java @@ -0,0 +1,40 @@ +package io.deephaven.server.jetty; + +import io.deephaven.configuration.Configuration; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.WebAppContext; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Consumer; + +import static org.eclipse.jetty.servlet.ServletContextHandler.NO_SESSIONS; + +class JsPlugins { + + public static void maybeAdd(Consumer addHandler) { + // Note: this would probably be better to live in JettyConfig - but until we establish more formal expectations + // for js plugin configuration and workflows, we'll keep this here. + final String resourceBase = + Configuration.getInstance().getStringWithDefault("deephaven.jsPlugins.resourceBase", null); + if (resourceBase == null) { + return; + } + try { + Resource resource = ControlledCacheResource.wrap(Resource.newResource(resourceBase)); + WebAppContext context = + new WebAppContext(null, "/js-plugins/", null, null, null, new ErrorPageErrorHandler(), NO_SESSIONS); + context.setBaseResource(resource); + context.setInitParameter(DefaultServlet.CONTEXT_INIT + "dirAllowed", "false"); + // Suppress warnings about security handlers + context.setSecurityHandler(new ConstraintSecurityHandler()); + addHandler.accept(context); + } catch (IOException e) { + throw new IllegalStateException(String.format("Unable to resolve resourceBase '%s'", resourceBase), e); + } + } +}