This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit dc32e659da395065488fe6c948824fd2706c2ce5 Author: TungTV <vtt...@linagora.com> AuthorDate: Tue Feb 11 10:48:58 2025 +0700 JAMES-4104 [webadmin] Clone some Jetty embedded class from Spark --- .../webadmin/jettyserver/EmbeddedJettyFactory.java | 71 +++++++ .../webadmin/jettyserver/EmbeddedJettyServer.java | 204 +++++++++++++++++++++ .../james/webadmin/jettyserver/JettyHandler.java | 62 +++++++ .../james/webadmin/jettyserver/JettyServer.java | 73 ++++++++ .../jettyserver/SocketConnectorFactory.java | 171 +++++++++++++++++ 5 files changed, 581 insertions(+) diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/EmbeddedJettyFactory.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/EmbeddedJettyFactory.java new file mode 100644 index 0000000000..c49acf2e0e --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/EmbeddedJettyFactory.java @@ -0,0 +1,71 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.webadmin.jettyserver; + +import org.eclipse.jetty.util.thread.ThreadPool; + +import spark.ExceptionMapper; +import spark.embeddedserver.EmbeddedServer; +import spark.embeddedserver.EmbeddedServerFactory; +import spark.embeddedserver.jetty.JettyServerFactory; +import spark.http.matching.MatcherFilter; +import spark.route.Routes; +import spark.staticfiles.StaticFilesConfiguration; + +public class EmbeddedJettyFactory implements EmbeddedServerFactory { + private final JettyServerFactory serverFactory; + private ThreadPool threadPool; + private boolean httpOnly = true; + + public EmbeddedJettyFactory() { + this(new JettyServer()); + } + + public EmbeddedJettyFactory(JettyServerFactory serverFactory) { + this.serverFactory = serverFactory; + } + + public EmbeddedServer create(Routes routeMatcher, + StaticFilesConfiguration staticFilesConfiguration, + ExceptionMapper exceptionMapper, + boolean hasMultipleHandler) { + MatcherFilter matcherFilter = new MatcherFilter(routeMatcher, staticFilesConfiguration, exceptionMapper, false, hasMultipleHandler); + matcherFilter.init(null); + + return new EmbeddedJettyServer(serverFactory, httpOnly, matcherFilter).withThreadPool(threadPool); + } + + /** + * Sets optional thread pool for jetty server. This is useful for overriding the default thread pool + * behaviour for example io.dropwizard.metrics.jetty9.InstrumentedQueuedThreadPool. + * + * @param threadPool thread pool + * @return Builder pattern - returns this instance + */ + public EmbeddedJettyFactory withThreadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + return this; + } + + public EmbeddedJettyFactory withHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } +} diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/EmbeddedJettyServer.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/EmbeddedJettyServer.java new file mode 100644 index 0000000000..3a4b6e9c64 --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/EmbeddedJettyServer.java @@ -0,0 +1,204 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.webadmin.jettyserver; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; + +import jakarta.servlet.DispatcherType; + +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.SessionHandler; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import spark.embeddedserver.EmbeddedServer; +import spark.embeddedserver.VirtualThreadAware; +import spark.embeddedserver.jetty.JettyServerFactory; +import spark.embeddedserver.jetty.SocketConnectorFactory; +import spark.embeddedserver.jetty.websocket.WebSocketHandlerWrapper; +import spark.embeddedserver.jetty.websocket.WebSocketServletContextHandlerFactory; +import spark.http.matching.MatcherFilter; +import spark.ssl.SslStores; + +/** + * Fork from spark.embeddedserver.jetty.EmbeddedJettyServer + */ +public class EmbeddedJettyServer extends VirtualThreadAware.Proxy implements EmbeddedServer { + + private static final int SPARK_DEFAULT_PORT = 4567; + private static final String NAME = "Spark"; + + private final JettyServerFactory serverFactory; + private final boolean httpOnly; + private final MatcherFilter matcherFilter; + private Server server; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private Map<String, WebSocketHandlerWrapper> webSocketHandlers; + private Optional<Long> webSocketIdleTimeoutMillis; + + private ThreadPool threadPool = null; + private boolean trustForwardHeaders = true; // true by default + + public EmbeddedJettyServer(JettyServerFactory serverFactory, boolean httpOnly, MatcherFilter matcherFilter) { + super(serverFactory); + this.serverFactory = serverFactory; + this.httpOnly = httpOnly; + this.matcherFilter = matcherFilter; + } + + @Override + public void configureWebSockets(Map<String, WebSocketHandlerWrapper> webSocketHandlers, + Optional<Long> webSocketIdleTimeoutMillis) { + + this.webSocketHandlers = webSocketHandlers; + this.webSocketIdleTimeoutMillis = webSocketIdleTimeoutMillis; + } + + @Override + public void trustForwardHeaders(boolean trust) { + this.trustForwardHeaders = trust; + } + + /** + * {@inheritDoc} + */ + @Override + public int ignite(String host, + int port, + boolean useHTTP2, + SslStores sslStores, + int maxThreads, + int minThreads, + int threadIdleTimeoutMillis) throws Exception { + + boolean hasCustomizedConnectors = false; + + if (port == 0) { + try (ServerSocket s = new ServerSocket(0)) { + port = s.getLocalPort(); + } catch (IOException e) { + logger.error("Could not get first available port (port set to 0), using default: {}", SPARK_DEFAULT_PORT); + port = SPARK_DEFAULT_PORT; + } + } + + // Create instance of jetty server with either default or supplied queued thread pool + if (threadPool == null) { + server = serverFactory.create(maxThreads, minThreads, threadIdleTimeoutMillis); + } else { + server = serverFactory.create(threadPool); + } + + ServerConnector connector; + + if (sslStores == null) { + connector = SocketConnectorFactory.createSocketConnector(server, host, port, useHTTP2, trustForwardHeaders); + } else { + connector = SocketConnectorFactory.createSecureSocketConnector(server, host, port, sslStores, useHTTP2, trustForwardHeaders); + } + + Connector[] previousConnectors = server.getConnectors(); + server = connector.getServer(); + if (previousConnectors.length != 0) { + server.setConnectors(previousConnectors); + hasCustomizedConnectors = true; + } else { + server.setConnectors(new Connector[]{connector}); + } + + final ServletContextHandler webSocketServletContextHandler = + WebSocketServletContextHandlerFactory.create(webSocketHandlers, webSocketIdleTimeoutMillis); + + final ServletContextHandler servletContextHandler = webSocketServletContextHandler == null ? + new ServletContextHandler() : webSocketServletContextHandler; + + final SessionHandler sessionHandler = new SessionHandler(); + sessionHandler.getSessionCookieConfig().setHttpOnly(httpOnly); + servletContextHandler.setSessionHandler(sessionHandler); + + servletContextHandler.addFilter(matcherFilter, "/*", EnumSet.allOf(DispatcherType.class)); + + server.setHandler(servletContextHandler); + + logger.info("== {} has ignited ...", NAME); + if (hasCustomizedConnectors) { + logger.info(">> Listening on Custom Server ports!"); + } else { + logger.info(">> Listening on {}:{}", host, port); + } + + server.start(); + return port; + } + + /** + * {@inheritDoc} + */ + @Override + public void join() throws InterruptedException { + server.join(); + } + + /** + * {@inheritDoc} + */ + @Override + public void extinguish() { + logger.info(">>> {} shutting down ...", NAME); + try { + if (server != null) { + server.stop(); + } + } catch (Exception e) { + logger.error("stop failed", e); + System.exit(100); // NOSONAR + } + logger.info("done"); + } + + @Override + public int activeThreadCount() { + if (server == null) { + return 0; + } + return server.getThreadPool().getThreads() - server.getThreadPool().getIdleThreads(); + } + + /** + * Sets optional thread pool for jetty server. This is useful for overriding the default thread pool + * behaviour for example io.dropwizard.metrics.jetty9.InstrumentedQueuedThreadPool. + * @param threadPool thread pool + * @return Builder pattern - returns this instance + */ + public EmbeddedJettyServer withThreadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + return this; + } +} \ No newline at end of file diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/JettyHandler.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/JettyHandler.java new file mode 100644 index 0000000000..94e01578bb --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/JettyHandler.java @@ -0,0 +1,62 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.webadmin.jettyserver; + +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.ee10.servlet.SessionHandler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +import spark.embeddedserver.jetty.HttpRequestWrapper; + +/** + * Simple Jetty Handler + * + * @author Per Wendel + * + * @see <a href="https://github.com/nmondal/spark-11/pull/20">Upstream issue</a> + */ +public class JettyHandler extends SessionHandler { + private final Filter filter; + + public JettyHandler(Filter filter) { + this.filter = filter; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + if (request instanceof ServletContextRequest servletContextRequest) { + final HttpServletResponse httpServletResponse = servletContextRequest.getHttpServletResponse(); + final HttpServletRequest httpServletRequest = servletContextRequest.getServletApiRequest(); + final HttpRequestWrapper wrapper = new HttpRequestWrapper(httpServletRequest); + + filter.doFilter(wrapper, httpServletResponse, null); + callback.succeeded(); + return true; + } + + return false; + } +} diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/JettyServer.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/JettyServer.java new file mode 100644 index 0000000000..aed89653f6 --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/JettyServer.java @@ -0,0 +1,73 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.webadmin.jettyserver; + + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.VirtualThreads; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +import spark.embeddedserver.VirtualThreadAware; +import spark.embeddedserver.jetty.JettyServerFactory; + +/** + * Creates Jetty Server instances. + * Clone from spark.embeddedserver.jetty.JettyServer + */ +class JettyServer extends VirtualThreadAware.Base implements JettyServerFactory { + + /** + * Creates a Jetty server. + * + * @param maxThreads maxThreads + * @param minThreads minThreads + * @param threadTimeoutMillis threadTimeoutMillis + * @return a new jetty server instance + */ + public Server create(int maxThreads, int minThreads, int threadTimeoutMillis) { + final QueuedThreadPool queuedThreadPool; + if (maxThreads > 0) { + int max = maxThreads; + int min = (minThreads > 0) ? minThreads : 8; + int idleTimeout = (threadTimeoutMillis > 0) ? threadTimeoutMillis : 60000; + queuedThreadPool = new QueuedThreadPool(max, min, idleTimeout); + } else { + queuedThreadPool = new QueuedThreadPool(); + } + return create(queuedThreadPool); + } + + /** + * Creates a Jetty server with supplied thread pool + * @param threadPool thread pool + * @return a new jetty server instance + */ + @Override + public Server create(ThreadPool threadPool) { + if (threadPool == null) { + threadPool = new QueuedThreadPool(); + } + if (useVThread && VirtualThreads.areSupported() && threadPool instanceof QueuedThreadPool) { + ((QueuedThreadPool) threadPool).setVirtualThreadsExecutor(VirtualThreads.getDefaultVirtualThreadsExecutor()); + } + return new Server(threadPool); + } +} diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/SocketConnectorFactory.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/SocketConnectorFactory.java new file mode 100644 index 0000000000..30e529fc26 --- /dev/null +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/jettyserver/SocketConnectorFactory.java @@ -0,0 +1,171 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.webadmin.jettyserver; + +import java.lang.reflect.Field; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.AbstractConnectionFactory; +import org.eclipse.jetty.server.ForwardedRequestCustomizer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import spark.ssl.SslStores; +import spark.utils.Assert; + +/** + * Fork from spark.embeddedserver.jetty.SocketConnectorFactory + * Creates socket connectors. + */ +public class SocketConnectorFactory { + + /** + * Creates an ordinary, non-secured Jetty server jetty. + * + * @param server Jetty server + * @param host host + * @param port port + * @param usHTTP2 if true use HTTP2 connection, else HTTP1.x + * @return - a server jetty + */ + public static ServerConnector createSocketConnector(Server server, String host, int port, boolean usHTTP2, boolean trustForwardHeaders) { + Assert.notNull(server, "'server' must not be null"); + Assert.notNull(host, "'host' must not be null"); + + final AbstractConnectionFactory connectionFactory = usHTTP2 ? + createHttp2ConnectionFactory(trustForwardHeaders) : createHttpConnectionFactory(trustForwardHeaders); + ServerConnector connector = new ServerConnector(server, connectionFactory); + initializeConnector(connector, host, port); + return connector; + } + + // jetty 12 verifies if a resource is readable, that breaks existing ( bad ) behaviour of tests + public static boolean ENABLE_JETTY_11_COMPATIBILITY = false; + + // a hacky way to insert non existing resource + static void forceInsertNonExistingResource(SslContextFactory.Server sslContextFactory, String someFieldName, String somePath) { + try { + Resource res = ResourceFactory.of(sslContextFactory).newResource(somePath); + Field myField = SslContextFactory.class.getDeclaredField(someFieldName); + myField.setAccessible(true); + myField.set(sslContextFactory, res); + } catch (Throwable ignore) { + // ignore + } + } + + // run if fails runs the default + static void runOrDefault(Runnable call, Runnable def) { + try { + call.run(); + } catch (RuntimeException rex) { + if (ENABLE_JETTY_11_COMPATIBILITY) { + def.run(); + } + } + } + + /** + * Creates a ssl jetty socket jetty. Keystore required, truststore + * optional. If truststore not specified keystore will be reused. + * + * @param server Jetty server + * @param sslStores the security sslStores. + * @param host host + * @param port port + * @param useHTTP2 if true return HTTP2 enabled connector, else return HTTP1.x connector + * @return a ssl socket jetty + */ + public static ServerConnector createSecureSocketConnector(Server server, + String host, + int port, + SslStores sslStores, + boolean useHTTP2, + boolean trustForwardHeaders) { + Assert.notNull(server, "'server' must not be null"); + Assert.notNull(host, "'host' must not be null"); + Assert.notNull(sslStores, "'sslStores' must not be null"); + + final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + runOrDefault(() -> sslContextFactory.setKeyStorePath(sslStores.keystoreFile()), () -> { + forceInsertNonExistingResource(sslContextFactory, "_keyStoreResource", sslStores.keystoreFile()); + }); + + if (sslStores.keystorePassword() != null) { + sslContextFactory.setKeyStorePassword(sslStores.keystorePassword()); + } + + if (sslStores.certAlias() != null) { + sslContextFactory.setCertAlias(sslStores.certAlias()); + } + + if (sslStores.trustStoreFile() != null) { + runOrDefault(() -> sslContextFactory.setTrustStorePath(sslStores.trustStoreFile()), () -> { + forceInsertNonExistingResource(sslContextFactory, "_trustStoreResource", sslStores.trustStoreFile()); + }); + } + + if (sslStores.trustStorePassword() != null) { + sslContextFactory.setTrustStorePassword(sslStores.trustStorePassword()); + } + + if (sslStores.needsClientCert()) { + sslContextFactory.setNeedClientAuth(true); + sslContextFactory.setWantClientAuth(true); + } + + HttpConnectionFactory httpConnectionFactory = createHttpConnectionFactory(trustForwardHeaders); + + ServerConnector connector = new ServerConnector(server, sslContextFactory, httpConnectionFactory); + initializeConnector(connector, host, port); + return connector; + } + + private static void initializeConnector(ServerConnector connector, String host, int port) { + // Set some timeout options to make debugging easier. + connector.setIdleTimeout(TimeUnit.HOURS.toMillis(1)); + connector.setHost(host); + connector.setPort(port); + } + + private static HttpConnectionFactory createHttpConnectionFactory(boolean trustForwardHeaders) { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + if (trustForwardHeaders) { + httpConfig.addCustomizer(new ForwardedRequestCustomizer()); + } + return new HttpConnectionFactory(httpConfig); + } + + private static HTTP2ServerConnectionFactory createHttp2ConnectionFactory(boolean trustForwardHeaders) { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + if (trustForwardHeaders) { + httpConfig.addCustomizer(new ForwardedRequestCustomizer()); + } + return new HTTP2ServerConnectionFactory(httpConfig); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org