Create spring-boot-jetty module
This commit is contained in:
committed by
Phillip Webb
parent
0a72db4676
commit
c96b7375fe
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
|
||||
|
||||
/**
|
||||
* {@link ConfigurableWebServerFactory} for Jetty-specific features.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServerFactory {
|
||||
|
||||
/**
|
||||
* Set the number of acceptor threads to use.
|
||||
* @param acceptors the number of acceptor threads to use
|
||||
*/
|
||||
void setAcceptors(int acceptors);
|
||||
|
||||
/**
|
||||
* Set the {@link ThreadPool} that should be used by the {@link Server}. If set to
|
||||
* {@code null} (default), the {@link Server} creates a {@link ThreadPool} implicitly.
|
||||
* @param threadPool the ThreadPool to be used
|
||||
*/
|
||||
void setThreadPool(ThreadPool threadPool);
|
||||
|
||||
/**
|
||||
* Set the number of selector threads to use.
|
||||
* @param selectors the number of selector threads to use
|
||||
*/
|
||||
void setSelectors(int selectors);
|
||||
|
||||
/**
|
||||
* Set if x-forward-* headers should be processed.
|
||||
* @param useForwardHeaders if x-forward headers should be used
|
||||
*/
|
||||
void setUseForwardHeaders(boolean useForwardHeaders);
|
||||
|
||||
/**
|
||||
* Add {@link JettyServerCustomizer}s that will be applied to the {@link Server}
|
||||
* before it is started.
|
||||
* @param customizers the customizers to add
|
||||
*/
|
||||
void addServerCustomizers(JettyServerCustomizer... customizers);
|
||||
|
||||
/**
|
||||
* Sets the maximum number of concurrent connections.
|
||||
* @param maxConnections the maximum number of concurrent connections
|
||||
* @since 4.0.0
|
||||
*/
|
||||
void setMaxConnections(int maxConnections);
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import org.eclipse.jetty.server.ConnectionFactory;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
|
||||
/**
|
||||
* {@link JettyServerCustomizer} to add {@link ForwardedRequestCustomizer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class ForwardHeadersCustomizer implements JettyServerCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(Server server) {
|
||||
ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
|
||||
for (Connector connector : server.getConnectors()) {
|
||||
for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
|
||||
if (connectionFactory instanceof HttpConfiguration.ConnectionFactory jettyConnectionFactory) {
|
||||
jettyConnectionFactory.getHttpConfiguration().addCustomizer(customizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
|
||||
import org.springframework.boot.web.server.GracefulShutdownCallback;
|
||||
import org.springframework.boot.web.server.GracefulShutdownResult;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Handles Jetty graceful shutdown.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Onur Kagan Ozcan
|
||||
*/
|
||||
final class GracefulShutdown {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GracefulShutdown.class);
|
||||
|
||||
private final Server server;
|
||||
|
||||
private final Supplier<Integer> activeRequests;
|
||||
|
||||
private volatile boolean aborted = false;
|
||||
|
||||
GracefulShutdown(Server server, Supplier<Integer> activeRequests) {
|
||||
this.server = server;
|
||||
this.activeRequests = activeRequests;
|
||||
}
|
||||
|
||||
void shutDownGracefully(GracefulShutdownCallback callback) {
|
||||
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
|
||||
new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start();
|
||||
boolean jetty10 = isJetty10();
|
||||
for (Connector connector : this.server.getConnectors()) {
|
||||
shutdown(connector, !jetty10);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void shutdown(Connector connector, boolean getResult) {
|
||||
Future<Void> result;
|
||||
try {
|
||||
result = connector.shutdown();
|
||||
}
|
||||
catch (NoSuchMethodError ex) {
|
||||
Method shutdown = ReflectionUtils.findMethod(connector.getClass(), "shutdown");
|
||||
result = (Future<Void>) ReflectionUtils.invokeMethod(shutdown, connector);
|
||||
}
|
||||
if (getResult) {
|
||||
try {
|
||||
result.get();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isJetty10() {
|
||||
try {
|
||||
return CompletableFuture.class.equals(Connector.class.getMethod("shutdown").getReturnType());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitShutdown(GracefulShutdownCallback callback) {
|
||||
while (!this.aborted && this.activeRequests.get() > 0) {
|
||||
sleep(100);
|
||||
}
|
||||
if (this.aborted) {
|
||||
logger.info("Graceful shutdown aborted with one or more requests still active");
|
||||
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
|
||||
}
|
||||
else {
|
||||
logger.info("Graceful shutdown complete");
|
||||
callback.shutdownComplete(GracefulShutdownResult.IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void abort() {
|
||||
this.aborted = true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields.Mutable;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
|
||||
/**
|
||||
* Jetty {@code HandlerWrapper} static factory.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
final class JettyHandlerWrappers {
|
||||
|
||||
private JettyHandlerWrappers() {
|
||||
}
|
||||
|
||||
static Handler.Wrapper createGzipHandlerWrapper(Compression compression) {
|
||||
GzipHandler handler = new GzipHandler();
|
||||
handler.setMinGzipSize((int) compression.getMinResponseSize().toBytes());
|
||||
handler.setIncludedMimeTypes(compression.getMimeTypes());
|
||||
for (HttpMethod httpMethod : HttpMethod.values()) {
|
||||
handler.addIncludedMethods(httpMethod.name());
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
static Handler.Wrapper createServerHeaderHandlerWrapper(String header) {
|
||||
return new ServerHeaderHandler(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Handler.Wrapper} to add a custom {@code server} header.
|
||||
*/
|
||||
private static class ServerHeaderHandler extends Handler.Wrapper {
|
||||
|
||||
private static final String SERVER_HEADER = "server";
|
||||
|
||||
private final String value;
|
||||
|
||||
ServerHeaderHandler(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception {
|
||||
Mutable headers = response.getHeaders();
|
||||
if (!headers.contains(SERVER_HEADER)) {
|
||||
headers.add(SERVER_HEADER, this.value);
|
||||
}
|
||||
return super.handle(request, response, callback);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
|
||||
import org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize a Jetty {@link Server}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 4.0.0
|
||||
* @see JettyServletWebServerFactory
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface JettyServerCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the server.
|
||||
* @param server the server to customize
|
||||
*/
|
||||
void customize(Server server);
|
||||
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.NetworkConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.server.handler.StatisticsHandler;
|
||||
|
||||
import org.springframework.boot.web.server.GracefulShutdownCallback;
|
||||
import org.springframework.boot.web.server.GracefulShutdownResult;
|
||||
import org.springframework.boot.web.server.PortInUseException;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.boot.web.server.reactive.jetty.JettyReactiveWebServerFactory;
|
||||
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link WebServer} that can be used to control a Jetty web server.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author David Liu
|
||||
* @author Eddú Meléndez
|
||||
* @author Brian Clozel
|
||||
* @author Kristine Jetzke
|
||||
* @since 4.0.0
|
||||
* @see JettyReactiveWebServerFactory
|
||||
*/
|
||||
public class JettyWebServer implements WebServer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JettyWebServer.class);
|
||||
|
||||
private final Object monitor = new Object();
|
||||
|
||||
private final Server server;
|
||||
|
||||
private final boolean autoStart;
|
||||
|
||||
private final GracefulShutdown gracefulShutdown;
|
||||
|
||||
private Connector[] connectors;
|
||||
|
||||
private volatile boolean started;
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyWebServer} instance.
|
||||
* @param server the underlying Jetty server
|
||||
*/
|
||||
public JettyWebServer(Server server) {
|
||||
this(server, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyWebServer} instance.
|
||||
* @param server the underlying Jetty server
|
||||
* @param autoStart if auto-starting the server
|
||||
*/
|
||||
public JettyWebServer(Server server, boolean autoStart) {
|
||||
this.autoStart = autoStart;
|
||||
Assert.notNull(server, "'server' must not be null");
|
||||
this.server = server;
|
||||
this.gracefulShutdown = createGracefulShutdown(server);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private GracefulShutdown createGracefulShutdown(Server server) {
|
||||
StatisticsHandler statisticsHandler = findStatisticsHandler(server);
|
||||
if (statisticsHandler == null) {
|
||||
return null;
|
||||
}
|
||||
return new GracefulShutdown(server, statisticsHandler::getRequestsActive);
|
||||
}
|
||||
|
||||
private StatisticsHandler findStatisticsHandler(Server server) {
|
||||
return findStatisticsHandler(server.getHandler());
|
||||
}
|
||||
|
||||
private StatisticsHandler findStatisticsHandler(Handler handler) {
|
||||
if (handler instanceof StatisticsHandler statisticsHandler) {
|
||||
return statisticsHandler;
|
||||
}
|
||||
if (handler instanceof Handler.Wrapper handlerWrapper) {
|
||||
return findStatisticsHandler(handlerWrapper.getHandler());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
synchronized (this.monitor) {
|
||||
try {
|
||||
// Cache the connectors and then remove them to prevent requests being
|
||||
// handled before the application context is ready.
|
||||
this.connectors = this.server.getConnectors();
|
||||
JettyWebServer.this.server.setConnectors(null);
|
||||
// Start the server so that the ServletContext is available
|
||||
this.server.start();
|
||||
this.server.setStopAtShutdown(false);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
// Ensure process isn't left running
|
||||
stopSilently();
|
||||
throw new WebServerException("Unable to start embedded Jetty web server", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSilently() {
|
||||
try {
|
||||
this.server.stop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws WebServerException {
|
||||
synchronized (this.monitor) {
|
||||
if (this.started) {
|
||||
return;
|
||||
}
|
||||
this.server.setConnectors(this.connectors);
|
||||
if (!this.autoStart) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.server.start();
|
||||
handleDeferredInitialize(this.server);
|
||||
Connector[] connectors = this.server.getConnectors();
|
||||
for (Connector connector : connectors) {
|
||||
try {
|
||||
connector.start();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (connector instanceof NetworkConnector networkConnector) {
|
||||
PortInUseException.throwIfPortBindingException(ex, networkConnector::getPort);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
this.started = true;
|
||||
logger.info(getStartedLogMessage());
|
||||
}
|
||||
catch (WebServerException ex) {
|
||||
stopSilently();
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
stopSilently();
|
||||
throw new WebServerException("Unable to start embedded Jetty server", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String getStartedLogMessage() {
|
||||
String contextPath = getContextPath();
|
||||
return "Jetty started on " + getActualPortsDescription()
|
||||
+ ((contextPath != null) ? " with context path '" + contextPath + "'" : "");
|
||||
}
|
||||
|
||||
private String getActualPortsDescription() {
|
||||
StringBuilder description = new StringBuilder("port");
|
||||
Connector[] connectors = this.server.getConnectors();
|
||||
if (connectors.length != 1) {
|
||||
description.append("s");
|
||||
}
|
||||
description.append(" ");
|
||||
for (int i = 0; i < connectors.length; i++) {
|
||||
if (i != 0) {
|
||||
description.append(", ");
|
||||
}
|
||||
Connector connector = connectors[i];
|
||||
description.append(getLocalPort(connector)).append(getProtocols(connector));
|
||||
}
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
private String getProtocols(Connector connector) {
|
||||
List<String> protocols = connector.getProtocols();
|
||||
return " (" + StringUtils.collectionToDelimitedString(protocols, ", ") + ")";
|
||||
}
|
||||
|
||||
private String getContextPath() {
|
||||
List<ContextHandler> imperativeContextHandlers = this.server.getHandlers()
|
||||
.stream()
|
||||
.map(this::findContextHandler)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(this::isImperative)
|
||||
.toList();
|
||||
if (imperativeContextHandlers.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return imperativeContextHandlers.stream().map(ContextHandler::getContextPath).collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
private ContextHandler findContextHandler(Handler handler) {
|
||||
while (handler instanceof Handler.Wrapper handlerWrapper) {
|
||||
if (handler instanceof ContextHandler contextHandler) {
|
||||
return contextHandler;
|
||||
}
|
||||
handler = handlerWrapper.getHandler();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isImperative(ContextHandler contextHandler) {
|
||||
if (contextHandler instanceof ServletContextHandler servletContextHandler) {
|
||||
Collection<ServletHolder> servletHolders = servletContextHandler.getServletHandler()
|
||||
.getBeans(ServletHolder.class);
|
||||
for (ServletHolder servletHolder : servletHolders) {
|
||||
if (ServletHttpHandlerAdapter.class.getName().equals(servletHolder.getClassName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs any necessary handling of deferred initialization.
|
||||
* @param server the server that has been started
|
||||
* @throws Exception if a failure occurs during the deferred initialization
|
||||
*/
|
||||
protected void handleDeferredInitialize(Server server) throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
synchronized (this.monitor) {
|
||||
this.started = false;
|
||||
if (this.gracefulShutdown != null) {
|
||||
this.gracefulShutdown.abort();
|
||||
}
|
||||
try {
|
||||
for (Connector connector : this.server.getConnectors()) {
|
||||
connector.stop();
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new WebServerException("Unable to stop embedded Jetty server", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
synchronized (this.monitor) {
|
||||
try {
|
||||
this.server.stop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new WebServerException("Unable to destroy embedded Jetty server", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
Connector[] connectors = this.server.getConnectors();
|
||||
for (Connector connector : connectors) {
|
||||
int localPort = getLocalPort(connector);
|
||||
if (localPort > 0) {
|
||||
return localPort;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int getLocalPort(Connector connector) {
|
||||
if (connector instanceof NetworkConnector networkConnector) {
|
||||
return networkConnector.getLocalPort();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a graceful shutdown of the Jetty web server. Handling of new requests is
|
||||
* prevented and the given {@code callback} is invoked at the end of the attempt. The
|
||||
* attempt can be explicitly ended by invoking {@link #stop}.
|
||||
* <p>
|
||||
* Once shutdown has been initiated Jetty will reject any new connections. Requests on
|
||||
* existing connections will be accepted, however, a {@code Connection: close} header
|
||||
* will be returned in the response.
|
||||
*/
|
||||
@Override
|
||||
public void shutDownGracefully(GracefulShutdownCallback callback) {
|
||||
if (this.gracefulShutdown == null) {
|
||||
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
|
||||
return;
|
||||
}
|
||||
this.gracefulShutdown.shutDownGracefully(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns access to the underlying Jetty Server.
|
||||
* @return the Jetty server
|
||||
*/
|
||||
public Server getServer() {
|
||||
return this.server;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.eclipse.jetty.ee10.webapp.Configuration;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.server.AbstractConnector;
|
||||
import org.eclipse.jetty.server.ConnectionFactory;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
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.thread.Scheduler;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
|
||||
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base class for factories that produce a {@link JettyWebServer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory
|
||||
implements ConfigurableJettyWebServerFactory {
|
||||
|
||||
private int acceptors = -1;
|
||||
|
||||
private ThreadPool threadPool;
|
||||
|
||||
private int selectors = -1;
|
||||
|
||||
private List<Configuration> configurations = new ArrayList<>();
|
||||
|
||||
private boolean useForwardHeaders;
|
||||
|
||||
private Set<JettyServerCustomizer> jettyServerCustomizers = new LinkedHashSet<>();
|
||||
|
||||
private int maxConnections = -1;
|
||||
|
||||
public JettyWebServerFactory() {
|
||||
|
||||
}
|
||||
|
||||
public JettyWebServerFactory(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
public int getAcceptors() {
|
||||
return this.acceptors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAcceptors(int acceptors) {
|
||||
this.acceptors = acceptors;
|
||||
}
|
||||
|
||||
public int getSelectors() {
|
||||
return this.selectors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectors(int selectors) {
|
||||
this.selectors = selectors;
|
||||
}
|
||||
|
||||
public int getMaxConnections() {
|
||||
return this.maxConnections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxConnections(int maxConnections) {
|
||||
this.maxConnections = maxConnections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of Jetty {@link JettyServerCustomizer}s that will be
|
||||
* applied to the {@link Server} before it is created.
|
||||
* @return the {@link JettyServerCustomizer}s
|
||||
*/
|
||||
public Collection<JettyServerCustomizer> getServerCustomizers() {
|
||||
return this.jettyServerCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server}
|
||||
* before it is started. Calling this method will replace any existing customizers.
|
||||
* @param customizers the Jetty customizers to apply
|
||||
*/
|
||||
public void setServerCustomizers(Collection<? extends JettyServerCustomizer> customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
this.jettyServerCustomizers = new LinkedHashSet<>(customizers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addServerCustomizers(JettyServerCustomizer... customizers) {
|
||||
Assert.notNull(customizers, "'customizers' must not be null");
|
||||
this.jettyServerCustomizers.addAll(Arrays.asList(customizers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable collection of Jetty {@link Configuration}s that will be applied
|
||||
* to the {@link WebAppContext} before the server is created.
|
||||
* @return the Jetty {@link Configuration}s
|
||||
*/
|
||||
public Collection<Configuration> getConfigurations() {
|
||||
return this.configurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Jetty {@link Configuration}s that will be applied to the {@link WebAppContext}
|
||||
* before the server is created. Calling this method will replace any existing
|
||||
* configurations.
|
||||
* @param configurations the Jetty configurations to apply
|
||||
*/
|
||||
public void setConfigurations(Collection<? extends Configuration> configurations) {
|
||||
Assert.notNull(configurations, "'configurations' must not be null");
|
||||
this.configurations = new ArrayList<>(configurations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link Configuration}s that will be applied to the {@link WebAppContext} before
|
||||
* the server is started.
|
||||
* @param configurations the configurations to add
|
||||
*/
|
||||
public void addConfigurations(Configuration... configurations) {
|
||||
Assert.notNull(configurations, "'configurations' must not be null");
|
||||
this.configurations.addAll(Arrays.asList(configurations));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Jetty {@link ThreadPool} that should be used by the {@link Server}.
|
||||
* @return a Jetty {@link ThreadPool} or {@code null}
|
||||
*/
|
||||
public ThreadPool getThreadPool() {
|
||||
return this.threadPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThreadPool(ThreadPool threadPool) {
|
||||
this.threadPool = threadPool;
|
||||
}
|
||||
|
||||
public boolean isUseForwardHeaders() {
|
||||
return this.useForwardHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseForwardHeaders(boolean useForwardHeaders) {
|
||||
this.useForwardHeaders = useForwardHeaders;
|
||||
}
|
||||
|
||||
protected AbstractConnector createConnector(InetSocketAddress address, Server server) {
|
||||
return this.createConnector(address, server, null, null, null);
|
||||
}
|
||||
|
||||
protected AbstractConnector createConnector(InetSocketAddress address, Server server, Executor executor,
|
||||
Scheduler scheduler, ByteBufferPool pool) {
|
||||
HttpConfiguration httpConfiguration = new HttpConfiguration();
|
||||
httpConfiguration.setSendServerVersion(false);
|
||||
List<ConnectionFactory> connectionFactories = new ArrayList<>();
|
||||
connectionFactories.add(new HttpConnectionFactory(httpConfiguration));
|
||||
if (getHttp2() != null && getHttp2().isEnabled()) {
|
||||
connectionFactories.add(new HTTP2CServerConnectionFactory(httpConfiguration));
|
||||
}
|
||||
ServerConnector connector = new ServerConnector(server, executor, scheduler, pool, this.getAcceptors(),
|
||||
this.getSelectors(), connectionFactories.toArray(new ConnectionFactory[0]));
|
||||
|
||||
connector.setHost(address.getHostString());
|
||||
connector.setPort(address.getPort());
|
||||
return connector;
|
||||
}
|
||||
|
||||
protected void customizeSsl(Server server, InetSocketAddress address) {
|
||||
Assert.state(getSsl().getServerNameBundles().isEmpty(), "Server name SSL bundles are not supported with Jetty");
|
||||
new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server);
|
||||
}
|
||||
|
||||
protected Handler addHandlerWrappers(Handler handler) {
|
||||
if (getCompression() != null && getCompression().getEnabled()) {
|
||||
handler = applyWrapper(handler, JettyHandlerWrappers.createGzipHandlerWrapper(getCompression()));
|
||||
}
|
||||
if (StringUtils.hasText(getServerHeader())) {
|
||||
handler = applyWrapper(handler, JettyHandlerWrappers.createServerHeaderHandlerWrapper(getServerHeader()));
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
protected Handler applyWrapper(Handler handler, Handler.Wrapper wrapper) {
|
||||
wrapper.setHandler(handler);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http2.HTTP2Cipher;
|
||||
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
|
||||
import org.eclipse.jetty.server.ConnectionFactory;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link JettyServerCustomizer} that configures SSL on the given Jetty server instance.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Olivier Lamy
|
||||
* @author Chris Bono
|
||||
* @author Cyril Dangerville
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class SslServerCustomizer implements JettyServerCustomizer {
|
||||
|
||||
private final Http2 http2;
|
||||
|
||||
private final InetSocketAddress address;
|
||||
|
||||
private final ClientAuth clientAuth;
|
||||
|
||||
private final SslBundle sslBundle;
|
||||
|
||||
SslServerCustomizer(Http2 http2, InetSocketAddress address, ClientAuth clientAuth, SslBundle sslBundle) {
|
||||
this.address = address;
|
||||
this.clientAuth = clientAuth;
|
||||
this.sslBundle = sslBundle;
|
||||
this.http2 = http2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Server server) {
|
||||
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setEndpointIdentificationAlgorithm(null);
|
||||
configureSsl(sslContextFactory, this.clientAuth);
|
||||
ServerConnector connector = createConnector(server, sslContextFactory);
|
||||
server.setConnectors(new Connector[] { connector });
|
||||
}
|
||||
|
||||
private ServerConnector createConnector(Server server, SslContextFactory.Server sslContextFactory) {
|
||||
HttpConfiguration config = new HttpConfiguration();
|
||||
config.setSendServerVersion(false);
|
||||
config.setSecureScheme("https");
|
||||
config.setSecurePort(this.address.getPort());
|
||||
config.addCustomizer(new SecureRequestCustomizer());
|
||||
ServerConnector connector = createServerConnector(server, sslContextFactory, config);
|
||||
connector.setPort(this.address.getPort());
|
||||
connector.setHost(this.address.getHostString());
|
||||
return connector;
|
||||
}
|
||||
|
||||
private ServerConnector createServerConnector(Server server, SslContextFactory.Server sslContextFactory,
|
||||
HttpConfiguration config) {
|
||||
if (this.http2 == null || !this.http2.isEnabled()) {
|
||||
return createHttp11ServerConnector(config, sslContextFactory, server);
|
||||
}
|
||||
Assert.state(isJettyAlpnPresent(),
|
||||
() -> "An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support.");
|
||||
Assert.state(isJettyHttp2Present(),
|
||||
() -> "The 'org.eclipse.jetty.http2:jetty-http2-server' dependency is required for HTTP/2 support.");
|
||||
return createHttp2ServerConnector(config, sslContextFactory, server);
|
||||
}
|
||||
|
||||
private ServerConnector createHttp11ServerConnector(HttpConfiguration config,
|
||||
SslContextFactory.Server sslContextFactory, Server server) {
|
||||
SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory,
|
||||
HttpVersion.HTTP_1_1.asString());
|
||||
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
|
||||
return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server,
|
||||
sslConnectionFactory, connectionFactory);
|
||||
}
|
||||
|
||||
private SslConnectionFactory createSslConnectionFactory(SslContextFactory.Server sslContextFactory,
|
||||
String protocol) {
|
||||
return new SslConnectionFactory(sslContextFactory, protocol);
|
||||
}
|
||||
|
||||
private boolean isJettyAlpnPresent() {
|
||||
return ClassUtils.isPresent("org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory", null);
|
||||
}
|
||||
|
||||
private boolean isJettyHttp2Present() {
|
||||
return ClassUtils.isPresent("org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory", null);
|
||||
}
|
||||
|
||||
private ServerConnector createHttp2ServerConnector(HttpConfiguration config,
|
||||
SslContextFactory.Server sslContextFactory, Server server) {
|
||||
HttpConnectionFactory http = new HttpConnectionFactory(config);
|
||||
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(config);
|
||||
ALPNServerConnectionFactory alpn = createAlpnServerConnectionFactory();
|
||||
sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
|
||||
if (isConscryptPresent()) {
|
||||
sslContextFactory.setProvider("Conscrypt");
|
||||
}
|
||||
SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory, alpn.getProtocol());
|
||||
return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server,
|
||||
sslConnectionFactory, alpn, h2, http);
|
||||
}
|
||||
|
||||
private ALPNServerConnectionFactory createAlpnServerConnectionFactory() {
|
||||
try {
|
||||
return new ALPNServerConnectionFactory();
|
||||
}
|
||||
catch (IllegalStateException ex) {
|
||||
throw new IllegalStateException(
|
||||
"An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConscryptPresent() {
|
||||
return ClassUtils.isPresent("org.conscrypt.Conscrypt", null)
|
||||
&& ClassUtils.isPresent("org.eclipse.jetty.alpn.conscrypt.server.ConscryptServerALPNProcessor", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the SSL connection.
|
||||
* @param factory the Jetty {@link Server SslContextFactory.Server}.
|
||||
* @param clientAuth the client authentication mode
|
||||
*/
|
||||
protected void configureSsl(SslContextFactory.Server factory, ClientAuth clientAuth) {
|
||||
SslBundleKey key = this.sslBundle.getKey();
|
||||
SslOptions options = this.sslBundle.getOptions();
|
||||
SslStoreBundle stores = this.sslBundle.getStores();
|
||||
factory.setProtocol(this.sslBundle.getProtocol());
|
||||
configureSslClientAuth(factory, clientAuth);
|
||||
if (stores.getKeyStorePassword() != null) {
|
||||
factory.setKeyStorePassword(stores.getKeyStorePassword());
|
||||
}
|
||||
factory.setCertAlias(key.getAlias());
|
||||
if (options.getCiphers() != null) {
|
||||
factory.setIncludeCipherSuites(options.getCiphers());
|
||||
factory.setExcludeCipherSuites();
|
||||
}
|
||||
if (options.getEnabledProtocols() != null) {
|
||||
factory.setIncludeProtocols(options.getEnabledProtocols());
|
||||
factory.setExcludeProtocols();
|
||||
}
|
||||
try {
|
||||
if (key.getPassword() != null) {
|
||||
factory.setKeyManagerPassword(key.getPassword());
|
||||
}
|
||||
factory.setKeyStore(stores.getKeyStore());
|
||||
factory.setTrustStore(stores.getTrustStore());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSslClientAuth(SslContextFactory.Server factory, ClientAuth clientAuth) {
|
||||
factory.setWantClientAuth(clientAuth == ClientAuth.WANT || clientAuth == ClientAuth.NEED);
|
||||
factory.setNeedClientAuth(clientAuth == ClientAuth.NEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ServerConnector} that validates the ssl key alias on server startup.
|
||||
*/
|
||||
static class SslValidatingServerConnector extends ServerConnector {
|
||||
|
||||
private final SslBundleKey key;
|
||||
|
||||
private final SslContextFactory sslContextFactory;
|
||||
|
||||
SslValidatingServerConnector(SslBundleKey key, SslContextFactory sslContextFactory, Server server,
|
||||
SslConnectionFactory sslConnectionFactory, HttpConnectionFactory connectionFactory) {
|
||||
super(server, sslConnectionFactory, connectionFactory);
|
||||
this.key = key;
|
||||
this.sslContextFactory = sslContextFactory;
|
||||
}
|
||||
|
||||
SslValidatingServerConnector(SslBundleKey keyAlias, SslContextFactory sslContextFactory, Server server,
|
||||
ConnectionFactory... factories) {
|
||||
super(server, factories);
|
||||
this.key = keyAlias;
|
||||
this.sslContextFactory = sslContextFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception {
|
||||
super.doStart();
|
||||
this.key.assertContainsAlias(this.sslContextFactory.getKeyStore());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reactive and servlet web server implementations backed by Jetty.
|
||||
*
|
||||
* @see org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory
|
||||
* @see org.springframework.boot.web.server.reactive.jetty.JettyReactiveWebServerFactory
|
||||
*/
|
||||
package org.springframework.boot.web.server.jetty;
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.reactive.jetty;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.server.ConnectionLimit;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.StatisticsHandler;
|
||||
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.jetty.ConfigurableJettyWebServerFactory;
|
||||
import org.springframework.boot.web.server.jetty.ForwardHeadersCustomizer;
|
||||
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServer;
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.jetty.JettyServletWebServerFactory;
|
||||
import org.springframework.http.client.reactive.JettyResourceFactory;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
|
||||
|
||||
/**
|
||||
* {@link ReactiveWebServerFactory} that can be used to create {@link JettyWebServer}s.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Moritz Halbritter
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class JettyReactiveWebServerFactory extends JettyWebServerFactory
|
||||
implements ConfigurableJettyWebServerFactory, ConfigurableReactiveWebServerFactory {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JettyReactiveWebServerFactory.class);
|
||||
|
||||
private JettyResourceFactory resourceFactory;
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyServletWebServerFactory} instance.
|
||||
*/
|
||||
public JettyReactiveWebServerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyServletWebServerFactory} that listens for requests using
|
||||
* the specified port.
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
public JettyReactiveWebServerFactory(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer(HttpHandler httpHandler) {
|
||||
ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(httpHandler);
|
||||
Server server = createJettyServer(servlet);
|
||||
return new JettyWebServer(server, getPort() >= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link JettyResourceFactory} to get the shared resources from.
|
||||
* @param resourceFactory the server resources
|
||||
*/
|
||||
public void setResourceFactory(JettyResourceFactory resourceFactory) {
|
||||
this.resourceFactory = resourceFactory;
|
||||
}
|
||||
|
||||
protected JettyResourceFactory getResourceFactory() {
|
||||
return this.resourceFactory;
|
||||
}
|
||||
|
||||
protected Server createJettyServer(ServletHttpHandlerAdapter servlet) {
|
||||
int port = Math.max(getPort(), 0);
|
||||
InetSocketAddress address = new InetSocketAddress(getAddress(), port);
|
||||
Server server = new Server(getThreadPool());
|
||||
if (this.resourceFactory == null) {
|
||||
server.addConnector(createConnector(address, server));
|
||||
}
|
||||
else {
|
||||
server.addConnector(createConnector(address, server, this.resourceFactory.getExecutor(),
|
||||
this.resourceFactory.getScheduler(), this.resourceFactory.getByteBufferPool()));
|
||||
}
|
||||
server.setStopTimeout(0);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
servletHolder.setAsyncSupported(true);
|
||||
ServletContextHandler contextHandler = new ServletContextHandler("/", false, false);
|
||||
contextHandler.addServlet(servletHolder, "/");
|
||||
server.setHandler(addHandlerWrappers(contextHandler));
|
||||
logger.info("Server initialized with port: " + port);
|
||||
if (this.getMaxConnections() > -1) {
|
||||
server.addBean(new ConnectionLimit(this.getMaxConnections(), server));
|
||||
}
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(server, address);
|
||||
}
|
||||
for (JettyServerCustomizer customizer : getServerCustomizers()) {
|
||||
customizer.customize(server);
|
||||
}
|
||||
if (this.isUseForwardHeaders()) {
|
||||
new ForwardHeadersCustomizer().customize(server);
|
||||
}
|
||||
if (getShutdown() == Shutdown.GRACEFUL) {
|
||||
StatisticsHandler statisticsHandler = new StatisticsHandler();
|
||||
statisticsHandler.setHandler(server.getHandler());
|
||||
server.setHandler(statisticsHandler);
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reactive web server implementation backed by Jetty.
|
||||
*
|
||||
*/
|
||||
package org.springframework.boot.web.server.reactive.jetty;
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
import java.net.URLStreamHandlerFactory;
|
||||
|
||||
import jakarta.servlet.ServletContainerInitializer;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
import org.eclipse.jetty.util.component.AbstractLifeCycle;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Jetty {@link AbstractLifeCycle} to initialize Jasper.
|
||||
*
|
||||
* @author Vladimir Tsanev
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JasperInitializer extends AbstractLifeCycle {
|
||||
|
||||
private static final String[] INITIALIZER_CLASSES = { "org.eclipse.jetty.apache.jsp.JettyJasperInitializer",
|
||||
"org.apache.jasper.servlet.JasperInitializer" };
|
||||
|
||||
private final WebAppContext context;
|
||||
|
||||
private final ServletContainerInitializer initializer;
|
||||
|
||||
JasperInitializer(WebAppContext context) {
|
||||
this.context = context;
|
||||
this.initializer = newInitializer();
|
||||
}
|
||||
|
||||
private ServletContainerInitializer newInitializer() {
|
||||
for (String className : INITIALIZER_CLASSES) {
|
||||
try {
|
||||
Class<?> initializerClass = ClassUtils.forName(className, null);
|
||||
return (ServletContainerInitializer) initializerClass.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() throws Exception {
|
||||
if (this.initializer == null) {
|
||||
return;
|
||||
}
|
||||
if (ClassUtils.isPresent("org.apache.catalina.webresources.TomcatURLStreamHandlerFactory",
|
||||
getClass().getClassLoader())) {
|
||||
org.apache.catalina.webresources.TomcatURLStreamHandlerFactory.register();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
URL.setURLStreamHandlerFactory(new WarUrlStreamHandlerFactory());
|
||||
}
|
||||
catch (Error ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
try {
|
||||
Thread.currentThread().setContextClassLoader(this.context.getClassLoader());
|
||||
try {
|
||||
this.context.getContext().setExtendedListenerTypes(true);
|
||||
this.initializer.onStartup(null, this.context.getServletContext());
|
||||
}
|
||||
finally {
|
||||
this.context.getContext().setExtendedListenerTypes(false);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread().setContextClassLoader(classLoader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link URLStreamHandlerFactory} to support {@literal war} protocol.
|
||||
*/
|
||||
private static final class WarUrlStreamHandlerFactory implements URLStreamHandlerFactory {
|
||||
|
||||
@Override
|
||||
public URLStreamHandler createURLStreamHandler(String protocol) {
|
||||
if ("war".equals(protocol)) {
|
||||
return new WarUrlStreamHandler();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link URLStreamHandler} for {@literal war} protocol compatible with jasper's
|
||||
* {@link URL urls} produced by
|
||||
* {@link org.apache.tomcat.util.scan.JarFactory#getJarEntryURL(URL, String)}.
|
||||
*/
|
||||
private static final class WarUrlStreamHandler extends URLStreamHandler {
|
||||
|
||||
@Override
|
||||
protected void parseURL(URL u, String spec, int start, int limit) {
|
||||
String path = "jar:" + spec.substring("war:".length());
|
||||
int separator = path.indexOf("*/");
|
||||
if (separator >= 0) {
|
||||
path = path.substring(0, separator) + "!/" + path.substring(separator + 2);
|
||||
}
|
||||
setURL(u, u.getProtocol(), "", -1, null, null, path, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL u) throws IOException {
|
||||
return new WarURLConnection(u);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link URLConnection} to support {@literal war} protocol.
|
||||
*/
|
||||
private static class WarURLConnection extends URLConnection {
|
||||
|
||||
private final URLConnection connection;
|
||||
|
||||
protected WarURLConnection(URL url) throws IOException {
|
||||
super(url);
|
||||
this.connection = new URL(url.getFile()).openConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
if (!this.connected) {
|
||||
this.connection.connect();
|
||||
this.connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
connect();
|
||||
return this.connection.getInputStream();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
|
||||
/**
|
||||
* Variation of Jetty's {@link ErrorPageErrorHandler} that supports all {@link HttpMethod
|
||||
* HttpMethods} rather than just {@code GET}, {@code POST} and {@code HEAD}. By default
|
||||
* Jetty <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=446039">intentionally only
|
||||
* supports a limited set of HTTP methods</a> for error pages, however, Spring Boot
|
||||
* prefers Tomcat, Jetty and Undertow to all behave in the same way.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Christoph Dreis
|
||||
*/
|
||||
class JettyEmbeddedErrorHandler extends ErrorPageErrorHandler {
|
||||
|
||||
@Override
|
||||
public boolean errorPageForMethod(String method) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
import org.eclipse.jetty.util.ClassMatcher;
|
||||
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServer;
|
||||
|
||||
/**
|
||||
* Jetty {@link WebAppContext} used by {@link JettyWebServer} to support deferred
|
||||
* initialization.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JettyEmbeddedWebAppContext extends WebAppContext {
|
||||
|
||||
JettyEmbeddedWebAppContext() {
|
||||
setHiddenClassMatcher(new ClassMatcher("org.springframework.boot.loader."));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServletHandler newServletHandler() {
|
||||
return new JettyEmbeddedServletHandler();
|
||||
}
|
||||
|
||||
void deferredInitialize() throws Exception {
|
||||
JettyEmbeddedServletHandler handler = (JettyEmbeddedServletHandler) getServletHandler();
|
||||
getContext().call(handler::deferredInitialize, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCanonicalNameForTmpDir() {
|
||||
return super.getCanonicalNameForTmpDir();
|
||||
}
|
||||
|
||||
private static final class JettyEmbeddedServletHandler extends ServletHandler {
|
||||
|
||||
@Override
|
||||
public void initialize() throws Exception {
|
||||
}
|
||||
|
||||
void deferredInitialize() throws Exception {
|
||||
super.initialize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServer;
|
||||
|
||||
/**
|
||||
* Servlet-specific {@link JettyWebServer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public class JettyServletWebServer extends JettyWebServer {
|
||||
|
||||
public JettyServletWebServer(Server server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
public JettyServletWebServer(Server server, boolean autoStart) {
|
||||
super(server, autoStart);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleDeferredInitialize(Server server) throws Exception {
|
||||
handleDeferredInitialize(server.getHandlers());
|
||||
}
|
||||
|
||||
protected void handleDeferredInitialize(List<Handler> handlers) throws Exception {
|
||||
for (Handler handler : handlers) {
|
||||
handleDeferredInitialize(handler);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeferredInitialize(Handler handler) throws Exception {
|
||||
if (handler instanceof JettyEmbeddedWebAppContext jettyEmbeddedWebAppContext) {
|
||||
jettyEmbeddedWebAppContext.deferredInitialize();
|
||||
}
|
||||
else if (handler instanceof Handler.Wrapper handlerWrapper) {
|
||||
handleDeferredInitialize(handlerWrapper.getHandler());
|
||||
}
|
||||
else if (handler instanceof Handler.Collection handlerCollection) {
|
||||
handleDeferredInitialize(handlerCollection.getHandlers());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,616 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.EventListener;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jetty.ee10.servlet.ErrorHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ListenerHolder;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletMapping;
|
||||
import org.eclipse.jetty.ee10.servlet.SessionHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.Source;
|
||||
import org.eclipse.jetty.ee10.webapp.AbstractConfiguration;
|
||||
import org.eclipse.jetty.ee10.webapp.Configuration;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
import org.eclipse.jetty.ee10.webapp.WebInfConfiguration;
|
||||
import org.eclipse.jetty.http.CookieCompliance;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpFields.Mutable;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.MimeTypes;
|
||||
import org.eclipse.jetty.http.MimeTypes.Wrapper;
|
||||
import org.eclipse.jetty.http.SetCookieParser;
|
||||
import org.eclipse.jetty.server.ConnectionLimit;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpCookieUtils;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.StatisticsHandler;
|
||||
import org.eclipse.jetty.session.DefaultSessionCache;
|
||||
import org.eclipse.jetty.session.FileSessionDataStore;
|
||||
import org.eclipse.jetty.session.SessionConfig;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.eclipse.jetty.util.resource.ResourceFactory;
|
||||
import org.eclipse.jetty.util.resource.URLResourceFactory;
|
||||
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.jetty.ConfigurableJettyWebServerFactory;
|
||||
import org.springframework.boot.web.server.jetty.ForwardHeadersCustomizer;
|
||||
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServer;
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ContextPath;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.DocumentRoot;
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializers;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerSettings;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link ServletWebServerFactory} that can be used to create a {@link JettyWebServer}.
|
||||
* Can be initialized using Spring's {@link ServletContextInitializer}s or Jetty
|
||||
* {@link Configuration}s.
|
||||
* <p>
|
||||
* Unless explicitly configured otherwise this factory will create servers that listen for
|
||||
* HTTP requests on port 8080.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andrey Hihlovskiy
|
||||
* @author Andy Wilkinson
|
||||
* @author Eddú Meléndez
|
||||
* @author Venil Noronha
|
||||
* @author Henri Kerola
|
||||
* @author Moritz Halbritter
|
||||
* @author Onur Kagan Ozcan
|
||||
* @since 4.0.0
|
||||
* @see #setPort(int)
|
||||
* @see #setConfigurations(Collection)
|
||||
* @see JettyWebServer
|
||||
*/
|
||||
public class JettyServletWebServerFactory extends JettyWebServerFactory
|
||||
implements ConfigurableJettyWebServerFactory, ConfigurableServletWebServerFactory, ResourceLoaderAware {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JettyServletWebServerFactory.class);
|
||||
|
||||
private final ServletWebServerSettings settings = new ServletWebServerSettings();
|
||||
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyServletWebServerFactory} instance.
|
||||
*/
|
||||
public JettyServletWebServerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyServletWebServerFactory} that listens for requests using
|
||||
* the specified port.
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
public JettyServletWebServerFactory(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link JettyServletWebServerFactory} with the specified context path
|
||||
* and port.
|
||||
* @param contextPath the root context path
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
public JettyServletWebServerFactory(String contextPath, int port) {
|
||||
super(port);
|
||||
getSettings().setContextPath(ContextPath.of(contextPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer(ServletContextInitializer... initializers) {
|
||||
JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext();
|
||||
context.getContext().getServletContext().setExtendedListenerTypes(true);
|
||||
int port = Math.max(getPort(), 0);
|
||||
InetSocketAddress address = new InetSocketAddress(getAddress(), port);
|
||||
Server server = createServer(address);
|
||||
context.setServer(server);
|
||||
configureWebAppContext(context, initializers);
|
||||
server.setHandler(addHandlerWrappers(context));
|
||||
logger.info("Server initialized with port: " + port);
|
||||
if (this.getMaxConnections() > -1) {
|
||||
server.addBean(new ConnectionLimit(this.getMaxConnections(), server.getConnectors()));
|
||||
}
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(server, address);
|
||||
}
|
||||
for (JettyServerCustomizer customizer : getServerCustomizers()) {
|
||||
customizer.customize(server);
|
||||
}
|
||||
if (this.isUseForwardHeaders()) {
|
||||
new ForwardHeadersCustomizer().customize(server);
|
||||
}
|
||||
if (getShutdown() == Shutdown.GRACEFUL) {
|
||||
StatisticsHandler statisticsHandler = new StatisticsHandler();
|
||||
statisticsHandler.setHandler(server.getHandler());
|
||||
server.setHandler(statisticsHandler);
|
||||
}
|
||||
return getJettyWebServer(server);
|
||||
}
|
||||
|
||||
private Server createServer(InetSocketAddress address) {
|
||||
Server server = new Server(getThreadPool());
|
||||
server.setConnectors(new Connector[] { createConnector(address, server) });
|
||||
server.setStopTimeout(0);
|
||||
MimeTypes.Mutable mimeTypes = server.getMimeTypes();
|
||||
for (MimeMappings.Mapping mapping : getSettings().getMimeMappings()) {
|
||||
mimeTypes.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Handler addHandlerWrappers(Handler handler) {
|
||||
handler = super.addHandlerWrappers(handler);
|
||||
if (!CollectionUtils.isEmpty(getSettings().getCookieSameSiteSuppliers())) {
|
||||
handler = applyWrapper(handler, new SuppliedSameSiteCookieHandlerWrapper(getSessionCookieName(),
|
||||
getSettings().getCookieSameSiteSuppliers()));
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
private String getSessionCookieName() {
|
||||
String name = getSettings().getSession().getCookie().getName();
|
||||
return (name != null) ? name : SessionConfig.__DefaultSessionCookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the given Jetty {@link WebAppContext} for use.
|
||||
* @param context the context to configure
|
||||
* @param initializers the set of initializers to apply
|
||||
*/
|
||||
protected final void configureWebAppContext(WebAppContext context, ServletContextInitializer... initializers) {
|
||||
Assert.notNull(context, "'context' must not be null");
|
||||
context.clearAliasChecks();
|
||||
if (this.resourceLoader != null) {
|
||||
context.setClassLoader(this.resourceLoader.getClassLoader());
|
||||
}
|
||||
String contextPath = getSettings().getContextPath().toString();
|
||||
context.setContextPath(StringUtils.hasLength(contextPath) ? contextPath : "/");
|
||||
context.setDisplayName(getSettings().getDisplayName());
|
||||
configureDocumentRoot(context);
|
||||
if (getSettings().isRegisterDefaultServlet()) {
|
||||
addDefaultServlet(context);
|
||||
}
|
||||
if (shouldRegisterJspServlet()) {
|
||||
addJspServlet(context);
|
||||
context.addBean(new JasperInitializer(context), true);
|
||||
}
|
||||
addLocaleMappings(context);
|
||||
ServletContextInitializers initializersToUse = ServletContextInitializers.from(this.settings, initializers);
|
||||
Configuration[] configurations = getWebAppContextConfigurations(context, initializersToUse);
|
||||
context.setConfigurations(configurations);
|
||||
context.setThrowUnavailableOnStartupException(true);
|
||||
configureSession(context);
|
||||
context.setTempDirectory(getTempDirectory(context));
|
||||
postProcessWebAppContext(context);
|
||||
}
|
||||
|
||||
private boolean shouldRegisterJspServlet() {
|
||||
return this.settings.getJsp() != null && this.settings.getJsp().getRegistered()
|
||||
&& ClassUtils.isPresent(this.settings.getJsp().getClassName(), getClass().getClassLoader());
|
||||
}
|
||||
|
||||
private void configureSession(WebAppContext context) {
|
||||
SessionHandler handler = context.getSessionHandler();
|
||||
SameSite sessionSameSite = getSettings().getSession().getCookie().getSameSite();
|
||||
if (sessionSameSite != null && sessionSameSite != SameSite.OMITTED) {
|
||||
handler.setSameSite(HttpCookie.SameSite.valueOf(sessionSameSite.name()));
|
||||
}
|
||||
Duration sessionTimeout = getSettings().getSession().getTimeout();
|
||||
handler.setMaxInactiveInterval(isNegative(sessionTimeout) ? -1 : (int) sessionTimeout.getSeconds());
|
||||
if (getSettings().getSession().isPersistent()) {
|
||||
DefaultSessionCache cache = new DefaultSessionCache(handler);
|
||||
FileSessionDataStore store = new FileSessionDataStore();
|
||||
store.setStoreDir(getSettings().getSession().getSessionStoreDirectory().getValidDirectory(true));
|
||||
cache.setSessionDataStore(store);
|
||||
handler.setSessionCache(cache);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNegative(Duration sessionTimeout) {
|
||||
return sessionTimeout == null || sessionTimeout.isNegative();
|
||||
}
|
||||
|
||||
private void addLocaleMappings(WebAppContext context) {
|
||||
getSettings().getLocaleCharsetMappings()
|
||||
.forEach((locale, charset) -> context.addLocaleEncoding(locale.toString(), charset.toString()));
|
||||
}
|
||||
|
||||
private File getTempDirectory(WebAppContext context) {
|
||||
String temp = System.getProperty("java.io.tmpdir");
|
||||
return (temp != null) ? new File(temp, getTempDirectoryPrefix(context) + UUID.randomUUID()) : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private String getTempDirectoryPrefix(WebAppContext context) {
|
||||
try {
|
||||
return ((JettyEmbeddedWebAppContext) context).getCanonicalNameForTmpDir();
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
return WebInfConfiguration.getCanonicalNameForWebAppTmpDir(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureDocumentRoot(WebAppContext handler) {
|
||||
DocumentRoot documentRoot = new DocumentRoot(logger);
|
||||
documentRoot.setDirectory(this.settings.getDocumentRoot());
|
||||
File root = documentRoot.getValidDirectory();
|
||||
File docBase = (root != null) ? root : createTempDir("jetty-docbase");
|
||||
try {
|
||||
ResourceFactory resourceFactory = handler.getResourceFactory();
|
||||
List<Resource> resources = new ArrayList<>();
|
||||
Resource rootResource = (docBase.isDirectory()
|
||||
? resourceFactory.newResource(docBase.getCanonicalFile().toURI())
|
||||
: resourceFactory.newJarFileResource(docBase.toURI()));
|
||||
resources.add((root != null) ? new LoaderHidingResource(rootResource, rootResource) : rootResource);
|
||||
URLResourceFactory urlResourceFactory = new URLResourceFactory();
|
||||
for (URL resourceJarUrl : getSettings().getStaticResourceUrls()) {
|
||||
Resource resource = createResource(resourceJarUrl, resourceFactory, urlResourceFactory);
|
||||
if (resource != null) {
|
||||
resources.add(resource);
|
||||
}
|
||||
}
|
||||
handler.setBaseResource(ResourceFactory.combine(resources));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Resource createResource(URL url, ResourceFactory resourceFactory, URLResourceFactory urlResourceFactory)
|
||||
throws Exception {
|
||||
if ("file".equals(url.getProtocol())) {
|
||||
File file = new File(url.toURI());
|
||||
if (file.isFile()) {
|
||||
return resourceFactory.newResource("jar:" + url + "!/META-INF/resources/");
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
return resourceFactory.newResource(url).resolve("META-INF/resources/");
|
||||
}
|
||||
}
|
||||
return urlResourceFactory.newResource(url + "META-INF/resources/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Jetty's {@code DefaultServlet} to the given {@link WebAppContext}.
|
||||
* @param context the jetty {@link WebAppContext}
|
||||
*/
|
||||
protected final void addDefaultServlet(WebAppContext context) {
|
||||
Assert.notNull(context, "'context' must not be null");
|
||||
ServletHolder holder = new ServletHolder();
|
||||
holder.setName("default");
|
||||
holder.setClassName("org.eclipse.jetty.ee10.servlet.DefaultServlet");
|
||||
holder.setInitParameter("dirAllowed", "false");
|
||||
holder.setInitOrder(1);
|
||||
context.getServletHandler().addServletWithMapping(holder, "/");
|
||||
ServletMapping servletMapping = context.getServletHandler().getServletMapping("/");
|
||||
servletMapping.setFromDefaultDescriptor(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Jetty's {@code JspServlet} to the given {@link WebAppContext}.
|
||||
* @param context the jetty {@link WebAppContext}
|
||||
*/
|
||||
protected final void addJspServlet(WebAppContext context) {
|
||||
Assert.notNull(context, "'context' must not be null");
|
||||
ServletHolder holder = new ServletHolder();
|
||||
holder.setName("jsp");
|
||||
holder.setClassName(this.settings.getJsp().getClassName());
|
||||
holder.setInitParameter("fork", "false");
|
||||
holder.setInitParameters(this.settings.getJsp().getInitParameters());
|
||||
holder.setInitOrder(3);
|
||||
context.getServletHandler().addServlet(holder);
|
||||
ServletMapping mapping = new ServletMapping();
|
||||
mapping.setServletName("jsp");
|
||||
mapping.setPathSpecs(new String[] { "*.jsp", "*.jspx" });
|
||||
context.getServletHandler().addServletMapping(mapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Jetty {@link Configuration}s that should be applied to the server.
|
||||
* @param webAppContext the Jetty {@link WebAppContext}
|
||||
* @param initializers the {@link ServletContextInitializer}s to apply
|
||||
* @return configurations to apply
|
||||
*/
|
||||
protected Configuration[] getWebAppContextConfigurations(WebAppContext webAppContext,
|
||||
ServletContextInitializers initializers) {
|
||||
List<Configuration> configurations = new ArrayList<>();
|
||||
configurations.add(getServletContextInitializerConfiguration(webAppContext, initializers));
|
||||
configurations.add(getErrorPageConfiguration());
|
||||
configurations.add(getMimeTypeConfiguration());
|
||||
configurations.add(new WebListenersConfiguration(getSettings().getWebListenerClassNames()));
|
||||
configurations.addAll(getConfigurations());
|
||||
return configurations.toArray(new Configuration[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a configuration object that adds error handlers.
|
||||
* @return a configuration object for adding error pages
|
||||
*/
|
||||
private Configuration getErrorPageConfiguration() {
|
||||
return new AbstractConfiguration(new AbstractConfiguration.Builder()) {
|
||||
|
||||
@Override
|
||||
public void configure(WebAppContext context) throws Exception {
|
||||
JettyEmbeddedErrorHandler errorHandler = new JettyEmbeddedErrorHandler();
|
||||
context.setErrorHandler(errorHandler);
|
||||
addJettyErrorPages(errorHandler, getErrorPages());
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a configuration object that adds mime type mappings.
|
||||
* @return a configuration object for adding mime type mappings
|
||||
*/
|
||||
private Configuration getMimeTypeConfiguration() {
|
||||
return new AbstractConfiguration(new AbstractConfiguration.Builder()) {
|
||||
|
||||
@Override
|
||||
public void configure(WebAppContext context) throws Exception {
|
||||
MimeTypes.Wrapper mimeTypes = (Wrapper) context.getMimeTypes();
|
||||
mimeTypes.setWrapped(new MimeTypes(null));
|
||||
for (MimeMappings.Mapping mapping : getSettings().getMimeMappings()) {
|
||||
mimeTypes.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Jetty {@link Configuration} that will invoke the specified
|
||||
* {@link ServletContextInitializer}s. By default this method will return a
|
||||
* {@link ServletContextInitializerConfiguration}.
|
||||
* @param webAppContext the Jetty {@link WebAppContext}
|
||||
* @param initializers the {@link ServletContextInitializer}s to apply
|
||||
* @return the {@link Configuration} instance
|
||||
*/
|
||||
protected Configuration getServletContextInitializerConfiguration(WebAppContext webAppContext,
|
||||
ServletContextInitializers initializers) {
|
||||
return new ServletContextInitializerConfiguration(initializers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process the Jetty {@link WebAppContext} before it's used with the Jetty
|
||||
* Server. Subclasses can override this method to apply additional processing to the
|
||||
* {@link WebAppContext}.
|
||||
* @param webAppContext the Jetty {@link WebAppContext}
|
||||
*/
|
||||
protected void postProcessWebAppContext(WebAppContext webAppContext) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method called to create the {@link JettyWebServer}. Subclasses can override
|
||||
* this method to return a different {@link JettyWebServer} or apply additional
|
||||
* processing to the Jetty server.
|
||||
* @param server the Jetty server.
|
||||
* @return a new {@link JettyWebServer} instance
|
||||
*/
|
||||
protected JettyWebServer getJettyWebServer(Server server) {
|
||||
return new JettyServletWebServer(server, getPort() >= 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
private void addJettyErrorPages(ErrorHandler errorHandler, Collection<ErrorPage> errorPages) {
|
||||
if (errorHandler instanceof ErrorPageErrorHandler handler) {
|
||||
for (ErrorPage errorPage : errorPages) {
|
||||
if (errorPage.isGlobal()) {
|
||||
handler.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE, errorPage.getPath());
|
||||
}
|
||||
else {
|
||||
if (errorPage.getExceptionName() != null) {
|
||||
handler.addErrorPage(errorPage.getExceptionName(), errorPage.getPath());
|
||||
}
|
||||
else {
|
||||
handler.addErrorPage(errorPage.getStatusCode(), errorPage.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletWebServerSettings getSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link AbstractConfiguration} to apply {@code @WebListener} classes.
|
||||
*/
|
||||
private static class WebListenersConfiguration extends AbstractConfiguration {
|
||||
|
||||
private final Set<String> classNames;
|
||||
|
||||
WebListenersConfiguration(Set<String> webListenerClassNames) {
|
||||
super(new AbstractConfiguration.Builder());
|
||||
this.classNames = webListenerClassNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(WebAppContext context) throws Exception {
|
||||
ServletHandler servletHandler = context.getServletHandler();
|
||||
for (String className : this.classNames) {
|
||||
configure(context, servletHandler, className);
|
||||
}
|
||||
}
|
||||
|
||||
private void configure(WebAppContext context, ServletHandler servletHandler, String className)
|
||||
throws ClassNotFoundException {
|
||||
ListenerHolder holder = servletHandler.newListenerHolder(new Source(Source.Origin.ANNOTATION, className));
|
||||
holder.setHeldClass(loadClass(context, className));
|
||||
servletHandler.addListener(holder);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Class<? extends EventListener> loadClass(WebAppContext context, String className)
|
||||
throws ClassNotFoundException {
|
||||
ClassLoader classLoader = context.getClassLoader();
|
||||
classLoader = (classLoader != null) ? classLoader : getClass().getClassLoader();
|
||||
return (Class<? extends EventListener>) classLoader.loadClass(className);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Handler.Wrapper} to apply {@link CookieSameSiteSupplier supplied}
|
||||
* {@link SameSite} cookie values.
|
||||
*/
|
||||
private static class SuppliedSameSiteCookieHandlerWrapper extends Handler.Wrapper {
|
||||
|
||||
private static final SetCookieParser setCookieParser = SetCookieParser.newInstance();
|
||||
|
||||
private final String sessionCookieName;
|
||||
|
||||
private final List<? extends CookieSameSiteSupplier> suppliers;
|
||||
|
||||
SuppliedSameSiteCookieHandlerWrapper(String sessionCookieName,
|
||||
List<? extends CookieSameSiteSupplier> suppliers) {
|
||||
this.sessionCookieName = sessionCookieName;
|
||||
this.suppliers = suppliers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception {
|
||||
SuppliedSameSiteCookieResponse wrappedResponse = new SuppliedSameSiteCookieResponse(request, response);
|
||||
return super.handle(request, wrappedResponse, callback);
|
||||
}
|
||||
|
||||
private class SuppliedSameSiteCookieResponse extends Response.Wrapper {
|
||||
|
||||
private final HttpFields.Mutable wrappedHeaders;
|
||||
|
||||
SuppliedSameSiteCookieResponse(Request request, Response wrapped) {
|
||||
super(request, wrapped);
|
||||
this.wrappedHeaders = new SuppliedSameSiteCookieHeaders(
|
||||
request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance(),
|
||||
wrapped.getHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mutable getHeaders() {
|
||||
return this.wrappedHeaders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SuppliedSameSiteCookieHeaders extends HttpFields.Mutable.Wrapper {
|
||||
|
||||
private final CookieCompliance compliance;
|
||||
|
||||
SuppliedSameSiteCookieHeaders(CookieCompliance compliance, HttpFields.Mutable fields) {
|
||||
super(fields);
|
||||
this.compliance = compliance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpField onAddField(HttpField field) {
|
||||
return (field.getHeader() != HttpHeader.SET_COOKIE) ? field : onAddSetCookieField(field);
|
||||
}
|
||||
|
||||
private HttpField onAddSetCookieField(HttpField field) {
|
||||
HttpCookie cookie = setCookieParser.parse(field.getValue());
|
||||
if (cookie == null || isSessionCookie(cookie)) {
|
||||
return field;
|
||||
}
|
||||
SameSite sameSite = getSameSite(cookie);
|
||||
if (sameSite == null) {
|
||||
return field;
|
||||
}
|
||||
HttpCookie updatedCookie = buildCookieWithUpdatedSameSite(cookie, sameSite);
|
||||
return new HttpCookieUtils.SetCookieHttpField(updatedCookie, this.compliance);
|
||||
}
|
||||
|
||||
private boolean isSessionCookie(HttpCookie cookie) {
|
||||
return SuppliedSameSiteCookieHandlerWrapper.this.sessionCookieName.equals(cookie.getName());
|
||||
}
|
||||
|
||||
private HttpCookie buildCookieWithUpdatedSameSite(HttpCookie cookie, SameSite sameSite) {
|
||||
return HttpCookie.build(cookie)
|
||||
.sameSite(org.eclipse.jetty.http.HttpCookie.SameSite.from(sameSite.name()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private SameSite getSameSite(HttpCookie cookie) {
|
||||
return getSameSite(asServletCookie(cookie));
|
||||
}
|
||||
|
||||
private SameSite getSameSite(Cookie cookie) {
|
||||
return SuppliedSameSiteCookieHandlerWrapper.this.suppliers.stream()
|
||||
.map((supplier) -> supplier.getSameSite(cookie))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Cookie asServletCookie(HttpCookie cookie) {
|
||||
Cookie servletCookie = new Cookie(cookie.getName(), cookie.getValue());
|
||||
cookie.getAttributes().forEach(servletCookie::setAttribute);
|
||||
return servletCookie;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Spliterator;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jetty.util.resource.CombinedResource;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
|
||||
/**
|
||||
* A custom {@link Resource} that hides Spring Boot's loader classes, preventing them from
|
||||
* being served over HTTP.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
final class LoaderHidingResource extends Resource {
|
||||
|
||||
private static final String LOADER_RESOURCE_PATH_PREFIX = "/org/springframework/boot/";
|
||||
|
||||
private final Path loaderBasePath;
|
||||
|
||||
private final Resource base;
|
||||
|
||||
private final Resource delegate;
|
||||
|
||||
LoaderHidingResource(Resource base, Resource delegate) {
|
||||
this.base = base;
|
||||
this.delegate = delegate;
|
||||
this.loaderBasePath = base.getPath().getFileSystem().getPath("/", "org", "springframework", "boot");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(Consumer<? super Resource> action) {
|
||||
this.delegate.forEach(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getPath() {
|
||||
return this.delegate.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContainedIn(Resource r) {
|
||||
return this.delegate.isContainedIn(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Resource> iterator() {
|
||||
if (this.delegate instanceof CombinedResource) {
|
||||
return list().iterator();
|
||||
}
|
||||
return List.<Resource>of(this).iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return this.delegate.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return this.delegate.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spliterator<Resource> spliterator() {
|
||||
return this.delegate.spliterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return this.delegate.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return this.delegate.isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant lastModified() {
|
||||
return this.delegate.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return this.delegate.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() {
|
||||
return this.delegate.getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileName() {
|
||||
return this.delegate.getFileName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream newInputStream() throws IOException {
|
||||
return this.delegate.newInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
public ReadableByteChannel newReadableByteChannel() throws IOException {
|
||||
return this.delegate.newReadableByteChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Resource> list() {
|
||||
return asLoaderHidingResources(this.delegate.list());
|
||||
}
|
||||
|
||||
private boolean nonLoaderResource(Resource resource) {
|
||||
return !resource.getPath().startsWith(this.loaderBasePath);
|
||||
}
|
||||
|
||||
private List<Resource> asLoaderHidingResources(Collection<Resource> resources) {
|
||||
return resources.stream().filter(this::nonLoaderResource).map(this::asLoaderHidingResource).toList();
|
||||
}
|
||||
|
||||
private Resource asLoaderHidingResource(Resource resource) {
|
||||
return (resource instanceof LoaderHidingResource) ? resource : new LoaderHidingResource(this.base, resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource resolve(String subUriPath) {
|
||||
if (subUriPath.startsWith(LOADER_RESOURCE_PATH_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
Resource resolved = this.delegate.resolve(subUriPath);
|
||||
return (resolved != null) ? new LoaderHidingResource(this.base, resolved) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlias() {
|
||||
return this.delegate.isAlias();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getRealURI() {
|
||||
return this.delegate.getRealURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(Path destination) throws IOException {
|
||||
this.delegate.copyTo(destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Resource> getAllResources() {
|
||||
return asLoaderHidingResources(this.delegate.getAllResources());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.delegate.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.eclipse.jetty.ee10.webapp.AbstractConfiguration;
|
||||
import org.eclipse.jetty.ee10.webapp.Configuration;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.server.servlet.ServletContextInitializers;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Jetty {@link Configuration} that calls {@link ServletContextInitializer}s.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class ServletContextInitializerConfiguration extends AbstractConfiguration {
|
||||
|
||||
private final ServletContextInitializers initializers;
|
||||
|
||||
/**
|
||||
* Create a new {@link ServletContextInitializerConfiguration}.
|
||||
* @param initializers the initializers that should be invoked
|
||||
*/
|
||||
ServletContextInitializerConfiguration(ServletContextInitializers initializers) {
|
||||
super(new AbstractConfiguration.Builder());
|
||||
Assert.notNull(initializers, "'initializers' must not be null");
|
||||
this.initializers = initializers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(WebAppContext context) throws Exception {
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
Thread.currentThread().setContextClassLoader(context.getClassLoader());
|
||||
try {
|
||||
callInitializers(context);
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread().setContextClassLoader(classLoader);
|
||||
}
|
||||
}
|
||||
|
||||
private void callInitializers(WebAppContext context) throws ServletException {
|
||||
try {
|
||||
context.getContext().setExtendedListenerTypes(true);
|
||||
for (ServletContextInitializer initializer : this.initializers) {
|
||||
initializer.onStartup(context.getServletContext());
|
||||
}
|
||||
}
|
||||
finally {
|
||||
context.getContext().setExtendedListenerTypes(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Servlet web server implementation backed by Jetty.
|
||||
*/
|
||||
package org.springframework.boot.web.server.servlet.jetty;
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
/**
|
||||
* Helper class to provide public access to package-private methods for testing purposes.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public final class JettyAccess {
|
||||
|
||||
private JettyAccess() {
|
||||
|
||||
}
|
||||
|
||||
public static String getStartedLogMessage(JettyWebServer jettyWebServer) {
|
||||
return jettyWebServer.getStartedLogMessage();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.jetty;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
|
||||
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
|
||||
import org.eclipse.jetty.server.ConnectionFactory;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.junit.DisabledOnOs;
|
||||
import org.springframework.boot.testsupport.ssl.MockPkcs11Security;
|
||||
import org.springframework.boot.testsupport.ssl.MockPkcs11SecurityProvider;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServerSslBundle;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslServerCustomizer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Cyril Dangerville
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@MockPkcs11Security
|
||||
class SslServerCustomizerTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
@WithPackageResources("test.jks")
|
||||
void whenHttp2IsNotEnabledServerConnectorHasSslAndHttpConnectionFactories() {
|
||||
Server server = createCustomizedServer();
|
||||
assertThat(server.getConnectors()).hasSize(1);
|
||||
List<ConnectionFactory> factories = new ArrayList<>(server.getConnectors()[0].getConnectionFactories());
|
||||
assertThat(factories).extracting((factory) -> (Class) factory.getClass())
|
||||
.containsExactly(SslConnectionFactory.class, HttpConnectionFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
@WithPackageResources("test.jks")
|
||||
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
|
||||
disabledReason = "conscrypt doesn't support Linux/macOS aarch64, see https://github.com/google/conscrypt/issues/1051")
|
||||
void whenHttp2IsEnabledServerConnectorsHasSslAlpnH2AndHttpConnectionFactories() {
|
||||
Http2 http2 = new Http2();
|
||||
http2.setEnabled(true);
|
||||
Server server = createCustomizedServer(http2);
|
||||
assertThat(server.getConnectors()).hasSize(1);
|
||||
List<ConnectionFactory> factories = new ArrayList<>(server.getConnectors()[0].getConnectionFactories());
|
||||
assertThat(factories).extracting((factory) -> (Class) factory.getClass())
|
||||
.containsExactly(SslConnectionFactory.class, ALPNServerConnectionFactory.class,
|
||||
HTTP2ServerConnectionFactory.class, HttpConnectionFactory.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
|
||||
disabledReason = "conscrypt doesn't support Linux/macOS aarch64, see https://github.com/google/conscrypt/issues/1051")
|
||||
void alpnConnectionFactoryHasNullDefaultProtocolToAllowNegotiationToHttp11() {
|
||||
Http2 http2 = new Http2();
|
||||
http2.setEnabled(true);
|
||||
Server server = createCustomizedServer(http2);
|
||||
assertThat(server.getConnectors()).hasSize(1);
|
||||
List<ConnectionFactory> factories = new ArrayList<>(server.getConnectors()[0].getConnectionFactories());
|
||||
assertThat(((ALPNServerConnectionFactory) factories.get(1)).getDefaultProtocol()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
|
||||
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
|
||||
}).withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void configureSslWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
|
||||
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
|
||||
}).withMessageContaining("must be empty or null for PKCS11 hardware key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStorePassword("1234");
|
||||
assertThatNoException().isThrownBy(() -> {
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
|
||||
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
|
||||
});
|
||||
}
|
||||
|
||||
private Server createCustomizedServer() {
|
||||
return createCustomizedServer(new Http2());
|
||||
}
|
||||
|
||||
private Server createCustomizedServer(Http2 http2) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
return createCustomizedServer(ssl, http2);
|
||||
}
|
||||
|
||||
private Server createCustomizedServer(Ssl ssl, Http2 http2) {
|
||||
Server server = new Server();
|
||||
new SslServerCustomizer(http2, new InetSocketAddress(0), ssl.getClientAuth(), WebServerSslBundle.get(ssl))
|
||||
.customize(server);
|
||||
return server;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.reactive.jetty;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.awaitility.Awaitility;
|
||||
import org.eclipse.jetty.server.ConnectionLimit;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
|
||||
import org.springframework.boot.web.server.jetty.JettyAccess;
|
||||
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServer;
|
||||
import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactoryTests;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyReactiveWebServerFactory} and {@link JettyWebServer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Madhura Bhave
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests {
|
||||
|
||||
@Override
|
||||
protected JettyReactiveWebServerFactory getFactory() {
|
||||
return new JettyReactiveWebServerFactory(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
@Disabled("Jetty 12 does not support User-Agent-based compression")
|
||||
// TODO Is this true with Jetty 12?
|
||||
protected void noCompressionForUserAgent() {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNullServerCustomizersShouldThrowException() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setServerCustomizers(null))
|
||||
.withMessageContaining("'customizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addNullServerCustomizersShouldThrowException() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> factory.addServerCustomizers((JettyServerCustomizer[]) null))
|
||||
.withMessageContaining("'customizers' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyCustomizersShouldBeInvoked() {
|
||||
HttpHandler handler = mock(HttpHandler.class);
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
JettyServerCustomizer[] configurations = new JettyServerCustomizer[4];
|
||||
Arrays.setAll(configurations, (i) -> mock(JettyServerCustomizer.class));
|
||||
factory.setServerCustomizers(Arrays.asList(configurations[0], configurations[1]));
|
||||
factory.addServerCustomizers(configurations[2], configurations[3]);
|
||||
this.webServer = factory.getWebServer(handler);
|
||||
InOrder ordered = inOrder((Object[]) configurations);
|
||||
for (JettyServerCustomizer configuration : configurations) {
|
||||
ordered.verify(configuration).customize(any(Server.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void specificIPAddressNotReverseResolved() throws Exception {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
factory.setAddress(InetAddress.getByAddress(localhost.getAddress()));
|
||||
this.webServer = factory.getWebServer(mock(HttpHandler.class));
|
||||
this.webServer.start();
|
||||
Connector connector = ((JettyWebServer) this.webServer).getServer().getConnectors()[0];
|
||||
assertThat(((ServerConnector) connector).getHost()).isEqualTo(localhost.getHostAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void useForwardedHeaders() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
factory.setUseForwardHeaders(true);
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingHandler blockingHandler = new BlockingHandler();
|
||||
this.webServer = factory.getWebServer(blockingHandler);
|
||||
this.webServer.start();
|
||||
WebClient webClient = getWebClient(this.webServer.getPort()).build();
|
||||
this.webServer.shutDownGracefully((result) -> {
|
||||
});
|
||||
Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> {
|
||||
blockingHandler.stopBlocking();
|
||||
try {
|
||||
webClient.get().retrieve().toBodilessEntity().block();
|
||||
return false;
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
return ex.getCause() instanceof ConnectException;
|
||||
}
|
||||
});
|
||||
this.webServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyMaxConnections() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
factory.setMaxConnections(1);
|
||||
this.webServer = factory.getWebServer(new EchoHandler());
|
||||
Server server = ((JettyWebServer) this.webServer).getServer();
|
||||
ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class);
|
||||
assertThat(connectionLimit).isNotNull();
|
||||
assertThat(connectionLimit.getMaxConnections()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sslServerNameBundlesConfigurationThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setBundle("test");
|
||||
List<ServerNameSslBundle> bundles = List.of(new ServerNameSslBundle("first", "test1"),
|
||||
new ServerNameSslBundle("second", "test2"));
|
||||
ssl.setServerNameBundles(bundles);
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
factory.setSsl(ssl);
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.webServer = factory.getWebServer(new EchoHandler()))
|
||||
.withMessageContaining("Server name SSL bundles are not supported with Jetty");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String startedLogMessage() {
|
||||
return JettyAccess.getStartedLogMessage((JettyWebServer) this.webServer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addConnector(int port, ConfigurableReactiveWebServerFactory factory) {
|
||||
((JettyReactiveWebServerFactory) factory).addServerCustomizers((server) -> {
|
||||
ServerConnector connector = new ServerConnector(server);
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EventListener;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jakarta.servlet.ServletContextEvent;
|
||||
import jakarta.servlet.ServletContextListener;
|
||||
import jakarta.servlet.ServletRegistration.Dynamic;
|
||||
import org.apache.hc.client5.http.HttpHostConnectException;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.jasper.servlet.JspServlet;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.ee10.webapp.AbstractConfiguration;
|
||||
import org.eclipse.jetty.ee10.webapp.Configuration;
|
||||
import org.eclipse.jetty.ee10.webapp.WebAppContext;
|
||||
import org.eclipse.jetty.server.AbstractConnector;
|
||||
import org.eclipse.jetty.server.ConnectionLimit;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.util.ClassMatcher;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
import org.springframework.boot.web.server.GracefulShutdownResult;
|
||||
import org.springframework.boot.web.server.PortInUseException;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
|
||||
import org.springframework.boot.web.server.WebServerException;
|
||||
import org.springframework.boot.web.server.jetty.JettyAccess;
|
||||
import org.springframework.boot.web.server.jetty.JettyServerCustomizer;
|
||||
import org.springframework.boot.web.server.jetty.JettyWebServer;
|
||||
import org.springframework.boot.web.server.servlet.AbstractServletWebServerFactoryTests;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyServletWebServerFactory}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @author Henri Kerola
|
||||
* @author Moritz Halbritter
|
||||
* @author Onur Kagan Ozcan
|
||||
*/
|
||||
class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests {
|
||||
|
||||
@Override
|
||||
protected JettyServletWebServerFactory getFactory() {
|
||||
JettyServletWebServerFactory factory = new JettyServletWebServerFactory(0);
|
||||
factory.addServerCustomizers((server) -> {
|
||||
for (Connector connector : server.getConnectors()) {
|
||||
if (connector instanceof ServerConnector serverConnector) {
|
||||
// TODO Set the shutdown idle timeout in main code?
|
||||
serverConnector.setShutdownIdleTimeout(10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addConnector(int port, ConfigurableServletWebServerFactory factory) {
|
||||
((JettyServletWebServerFactory) factory).addServerCustomizers((server) -> {
|
||||
ServerConnector connector = new ServerConnector(server);
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JspServlet getJspServlet() throws Exception {
|
||||
WebAppContext context = findWebAppContext((JettyWebServer) this.webServer);
|
||||
ServletHolder holder = context.getServletHandler().getServlet("jsp");
|
||||
if (holder == null) {
|
||||
return null;
|
||||
}
|
||||
holder.start();
|
||||
holder.initialize();
|
||||
return (JspServlet) holder.getServlet();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, String> getActualMimeMappings() {
|
||||
WebAppContext context = findWebAppContext((JettyWebServer) this.webServer);
|
||||
return context.getMimeTypes().getMimeMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Charset getCharset(Locale locale) {
|
||||
WebAppContext context = findWebAppContext((JettyWebServer) this.webServer);
|
||||
String charsetName = context.getLocaleEncoding(locale);
|
||||
return (charsetName != null) ? Charset.forName(charsetName) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExceptionCausedByBlockedPortOnPrimaryConnector(RuntimeException ex, int blockedPort) {
|
||||
assertThat(ex).isInstanceOf(PortInUseException.class);
|
||||
assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExceptionCausedByBlockedPortOnSecondaryConnector(RuntimeException ex, int blockedPort) {
|
||||
handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
@Disabled("Jetty 12 does not support User-Agent-based compression")
|
||||
protected void noCompressionForUserAgent() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
@Disabled("Jetty 12 does not support SSL session tracking")
|
||||
protected void sslSessionTracking() {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) {
|
||||
ConfigurableServletWebServerFactory factory = getFactory();
|
||||
factory.setContextPath("/custom");
|
||||
Compression compression = new Compression();
|
||||
compression.setEnabled(true);
|
||||
factory.setCompression(compression);
|
||||
this.webServer = factory.getWebServer(exampleServletRegistration());
|
||||
this.webServer.start();
|
||||
assertThat(output).containsOnlyOnce("with context path '/custom'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyConfigurations() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
Configuration[] configurations = new Configuration[] { mockConfiguration(Configuration1.class),
|
||||
mockConfiguration(Configuration2.class), mockConfiguration(Configuration3.class),
|
||||
mockConfiguration(Configuration4.class) };
|
||||
factory.setConfigurations(Arrays.asList(configurations[0], configurations[1]));
|
||||
factory.addConfigurations(configurations[2], configurations[3]);
|
||||
this.webServer = factory.getWebServer();
|
||||
InOrder ordered = inOrder((Object[]) configurations);
|
||||
for (Configuration configuration : configurations) {
|
||||
ordered.verify(configuration).configure(any(WebAppContext.class));
|
||||
}
|
||||
}
|
||||
|
||||
Configuration mockConfiguration(Class<? extends Configuration> type) {
|
||||
Configuration mock = mock(type);
|
||||
ClassMatcher classMatcher = new ClassMatcher();
|
||||
given(mock.getProtectedClasses()).willReturn(classMatcher);
|
||||
given(mock.getHiddenClasses()).willReturn(classMatcher);
|
||||
return mock;
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyCustomizations() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
JettyServerCustomizer[] configurations = new JettyServerCustomizer[4];
|
||||
Arrays.setAll(configurations, (i) -> mock(JettyServerCustomizer.class));
|
||||
factory.setServerCustomizers(Arrays.asList(configurations[0], configurations[1]));
|
||||
factory.addServerCustomizers(configurations[2], configurations[3]);
|
||||
this.webServer = factory.getWebServer();
|
||||
InOrder ordered = inOrder((Object[]) configurations);
|
||||
for (JettyServerCustomizer configuration : configurations) {
|
||||
ordered.verify(configuration).customize(any(Server.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionTimeout() {
|
||||
ConfigurableServletWebServerFactory factory = getFactory();
|
||||
factory.getSettings().getSession().setTimeout(Duration.ofSeconds(10));
|
||||
assertTimeout(factory, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionTimeoutInMinutes() {
|
||||
ConfigurableServletWebServerFactory factory = getFactory();
|
||||
factory.getSettings().getSession().setTimeout(Duration.ofMinutes(1));
|
||||
assertTimeout(factory, 60);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslCiphersConfiguration(@ResourcePath("test.jks") String keyStore) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore(keyStore);
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" });
|
||||
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setSsl(ssl);
|
||||
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
|
||||
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
|
||||
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
|
||||
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
|
||||
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
|
||||
assertThat(sslContextFactory.getIncludeCipherSuites()).containsExactly("ALPHA", "BRAVO", "CHARLIE");
|
||||
assertThat(sslContextFactory.getExcludeCipherSuites()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void destroyCalledWithoutStart() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
this.webServer = factory.getWebServer(exampleServletRegistration());
|
||||
this.webServer.destroy();
|
||||
Server server = ((JettyWebServer) this.webServer).getServer();
|
||||
assertThat(server.isStopped()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslEnabledMultiProtocolsConfiguration(@ResourcePath("test.jks") String keyStore) {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setSsl(getSslSettings(keyStore, "TLSv1.1", "TLSv1.2"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
|
||||
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
|
||||
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
|
||||
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
|
||||
assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void sslEnabledProtocolsConfiguration(@ResourcePath("test.jks") String keyStore) {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setSsl(getSslSettings(keyStore, "TLSv1.1"));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
|
||||
ServerConnector connector = (ServerConnector) jettyWebServer.getServer().getConnectors()[0];
|
||||
SslConnectionFactory connectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
|
||||
SslContextFactory sslContextFactory = extractSslContextFactory(connectionFactory);
|
||||
assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sslServerNameBundlesConfigurationThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setBundle("test");
|
||||
List<ServerNameSslBundle> bundles = List.of(new ServerNameSslBundle("first", "test1"),
|
||||
new ServerNameSslBundle("second", "test2"));
|
||||
ssl.setServerNameBundles(bundles);
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setSsl(ssl);
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.webServer = factory.getWebServer())
|
||||
.withMessageContaining("Server name SSL bundles are not supported with Jetty");
|
||||
}
|
||||
|
||||
private SslContextFactory extractSslContextFactory(SslConnectionFactory connectionFactory) {
|
||||
try {
|
||||
return connectionFactory.getSslContextFactory();
|
||||
}
|
||||
catch (NoSuchMethodError ex) {
|
||||
Method getSslContextFactory = ReflectionUtils.findMethod(connectionFactory.getClass(),
|
||||
"getSslContextFactory");
|
||||
return (SslContextFactory) ReflectionUtils.invokeMethod(getSslContextFactory, connectionFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() throws Exception {
|
||||
ConfigurableServletWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingServlet blockingServlet = new BlockingServlet();
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
|
||||
registration.addMapping("/blocking");
|
||||
registration.setAsyncSupported(true);
|
||||
});
|
||||
this.webServer.start();
|
||||
int port = this.webServer.getPort();
|
||||
Future<Object> request = initiateGetRequest(port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
this.webServer.shutDownGracefully((result) -> {
|
||||
});
|
||||
Future<Object> unconnectableRequest = initiateGetRequest(port, "/");
|
||||
blockingServlet.admitOne();
|
||||
Object response = request.get();
|
||||
assertThat(response).isInstanceOf(HttpResponse.class);
|
||||
assertThat(unconnectableRequest.get()).isInstanceOf(HttpHostConnectException.class);
|
||||
this.webServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownGracefullyThenResponseToRequestOnIdleConnectionWillHaveAConnectionCloseHeader()
|
||||
throws Exception {
|
||||
ConfigurableServletWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingServlet blockingServlet = new BlockingServlet();
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
|
||||
registration.addMapping("/blocking");
|
||||
registration.setAsyncSupported(true);
|
||||
});
|
||||
this.webServer.start();
|
||||
int port = this.webServer.getPort();
|
||||
HttpClient client = HttpClients.createMinimal();
|
||||
Future<Object> request = initiateGetRequest(client, port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
blockingServlet.admitOne();
|
||||
Object response = request.get();
|
||||
assertThat(response).isInstanceOf(HttpResponse.class);
|
||||
assertThat(((HttpResponse) response).getCode()).isEqualTo(200);
|
||||
assertThat(((HttpResponse) response).getFirstHeader("Connection")).isNull();
|
||||
this.webServer.shutDownGracefully((result) -> {
|
||||
});
|
||||
request = initiateGetRequest(client, port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
blockingServlet.admitOne();
|
||||
response = request.get();
|
||||
assertThat(response).isInstanceOf(HttpResponse.class);
|
||||
assertThat(((HttpResponse) response).getCode()).isEqualTo(200);
|
||||
assertThat(((HttpResponse) response).getFirstHeader("Connection")).isNotNull()
|
||||
.extracting(Header::getValue)
|
||||
.isEqualTo("close");
|
||||
this.webServer.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenARequestCompletesAfterGracefulShutdownHasBegunThenItHasAConnectionCloseHeader()
|
||||
throws InterruptedException, ExecutionException {
|
||||
ConfigurableServletWebServerFactory factory = getFactory();
|
||||
factory.setShutdown(Shutdown.GRACEFUL);
|
||||
BlockingServlet blockingServlet = new BlockingServlet();
|
||||
this.webServer = factory.getWebServer((context) -> {
|
||||
Dynamic registration = context.addServlet("blockingServlet", blockingServlet);
|
||||
registration.addMapping("/blocking");
|
||||
registration.setAsyncSupported(true);
|
||||
});
|
||||
this.webServer.start();
|
||||
int port = this.webServer.getPort();
|
||||
Future<Object> request = initiateGetRequest(port, "/blocking");
|
||||
blockingServlet.awaitQueue();
|
||||
AtomicReference<GracefulShutdownResult> result = new AtomicReference<>();
|
||||
this.webServer.shutDownGracefully(result::set);
|
||||
blockingServlet.admitOne();
|
||||
Awaitility.await().atMost(Duration.ofSeconds(5)).until(() -> GracefulShutdownResult.IDLE == result.get());
|
||||
Object requestResult = request.get();
|
||||
assertThat(requestResult).isInstanceOf(HttpResponse.class);
|
||||
assertThat(((HttpResponse) requestResult).getFirstHeader("Connection").getValue()).isEqualTo("close");
|
||||
}
|
||||
|
||||
private Ssl getSslSettings(String keyStore, String... enabledProtocols) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore(keyStore);
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" });
|
||||
ssl.setEnabledProtocols(enabledProtocols);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
private void assertTimeout(ConfigurableServletWebServerFactory factory, int expected) {
|
||||
this.webServer = factory.getWebServer();
|
||||
JettyWebServer jettyWebServer = (JettyWebServer) this.webServer;
|
||||
WebAppContext webAppContext = findWebAppContext(jettyWebServer);
|
||||
int actual = webAppContext.getSessionHandler().getMaxInactiveInterval();
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrappedHandlers() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setServerCustomizers(Collections.singletonList((server) -> {
|
||||
Handler handler = server.getHandler();
|
||||
Handler.Wrapper wrapper = new Handler.Wrapper();
|
||||
wrapper.setHandler(handler);
|
||||
server.setHandler(wrapper);
|
||||
}));
|
||||
this.webServer = factory.getWebServer(exampleServletRegistration());
|
||||
this.webServer.start();
|
||||
assertThat(getResponse(getLocalUrl("/hello"))).isEqualTo("Hello World");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void basicSslClasspathKeyStore() throws Exception {
|
||||
testBasicSslWithKeyStore("classpath:test.jks");
|
||||
}
|
||||
|
||||
@Test
|
||||
void useForwardHeaders() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setUseForwardHeaders(true);
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultThreadPool() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setThreadPool(null);
|
||||
assertThat(factory.getThreadPool()).isNull();
|
||||
this.webServer = factory.getWebServer();
|
||||
assertThat(((JettyWebServer) this.webServer).getServer().getThreadPool()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customThreadPool() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
ThreadPool threadPool = mock(ThreadPool.class);
|
||||
factory.setThreadPool(threadPool);
|
||||
this.webServer = factory.getWebServer();
|
||||
assertThat(((JettyWebServer) this.webServer).getServer().getThreadPool()).isSameAs(threadPool);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startFailsWhenThreadPoolIsTooSmall() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.addServerCustomizers((server) -> {
|
||||
QueuedThreadPool threadPool = server.getBean(QueuedThreadPool.class);
|
||||
threadPool.setMaxThreads(2);
|
||||
threadPool.setMinThreads(2);
|
||||
});
|
||||
assertThatExceptionOfType(WebServerException.class).isThrownBy(factory.getWebServer()::start)
|
||||
.withCauseInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void specificIPAddressNotReverseResolved() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
factory.setAddress(InetAddress.getByAddress(localhost.getAddress()));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
Connector connector = ((JettyWebServer) this.webServer).getServer().getConnectors()[0];
|
||||
assertThat(((ServerConnector) connector).getHost()).isEqualTo(localhost.getHostAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.jks")
|
||||
void specificIPAddressWithSslIsNotReverseResolved() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
InetAddress localhost = InetAddress.getLocalHost();
|
||||
factory.setAddress(InetAddress.getByAddress(localhost.getAddress()));
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setKeyPassword("password");
|
||||
factory.setSsl(ssl);
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
Connector connector = ((JettyWebServer) this.webServer).getServer().getConnectors()[0];
|
||||
assertThat(((ServerConnector) connector).getHost()).isEqualTo(localhost.getHostAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void faultyListenerCausesStartFailure() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.addServerCustomizers((JettyServerCustomizer) (server) -> {
|
||||
Collection<WebAppContext> contexts = server.getBeans(WebAppContext.class);
|
||||
EventListener eventListener = new ServletContextListener() {
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent event) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent event) {
|
||||
}
|
||||
};
|
||||
WebAppContext context = contexts.iterator().next();
|
||||
try {
|
||||
context.addEventListener(eventListener);
|
||||
}
|
||||
catch (NoSuchMethodError ex) {
|
||||
// Jetty 10
|
||||
Method addEventListener = ReflectionUtils.findMethod(context.getClass(), "addEventListener",
|
||||
EventListener.class);
|
||||
ReflectionUtils.invokeMethod(addEventListener, context, eventListener);
|
||||
}
|
||||
});
|
||||
assertThatExceptionOfType(WebServerException.class).isThrownBy(() -> {
|
||||
JettyWebServer jettyWebServer = (JettyWebServer) factory.getWebServer();
|
||||
try {
|
||||
jettyWebServer.start();
|
||||
}
|
||||
finally {
|
||||
QueuedThreadPool threadPool = (QueuedThreadPool) jettyWebServer.getServer().getThreadPool();
|
||||
assertThat(threadPool.isRunning()).isFalse();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorHandlerCanBeOverridden() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.addConfigurations(new AbstractConfiguration(new AbstractConfiguration.Builder()) {
|
||||
|
||||
@Override
|
||||
public void configure(WebAppContext context) throws Exception {
|
||||
context.setErrorHandler(new CustomErrorHandler());
|
||||
}
|
||||
|
||||
});
|
||||
JettyWebServer jettyWebServer = (JettyWebServer) factory.getWebServer();
|
||||
WebAppContext context = findWebAppContext(jettyWebServer);
|
||||
assertThat(context.getErrorHandler()).isInstanceOf(CustomErrorHandler.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyMaxConnections() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setMaxConnections(1);
|
||||
this.webServer = factory.getWebServer();
|
||||
Server server = ((JettyWebServer) this.webServer).getServer();
|
||||
ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class);
|
||||
assertThat(connectionLimit).isNotNull();
|
||||
assertThat(connectionLimit.getMaxConnections()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyMaxConnectionsToConnectors() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setMaxConnections(1);
|
||||
this.webServer = factory.getWebServer();
|
||||
Server server = ((JettyWebServer) this.webServer).getServer();
|
||||
assertThat(server.getConnectors()).isEmpty();
|
||||
ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class);
|
||||
assertThat(connectionLimit).extracting("_connectors")
|
||||
.asInstanceOf(InstanceOfAssertFactories.list(AbstractConnector.class))
|
||||
.hasSize(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String startedLogMessage() {
|
||||
return JettyAccess.getStartedLogMessage((JettyWebServer) this.webServer);
|
||||
}
|
||||
|
||||
private WebAppContext findWebAppContext(JettyWebServer webServer) {
|
||||
return findWebAppContext(webServer.getServer().getHandler());
|
||||
}
|
||||
|
||||
private WebAppContext findWebAppContext(Handler handler) {
|
||||
if (handler instanceof WebAppContext webAppContext) {
|
||||
return webAppContext;
|
||||
}
|
||||
if (handler instanceof Handler.Wrapper wrapper) {
|
||||
return findWebAppContext(wrapper.getHandler());
|
||||
}
|
||||
throw new IllegalStateException("No WebAppContext found");
|
||||
}
|
||||
|
||||
private static final class CustomErrorHandler extends ErrorPageErrorHandler {
|
||||
|
||||
}
|
||||
|
||||
interface Configuration1 extends Configuration {
|
||||
|
||||
}
|
||||
|
||||
interface Configuration2 extends Configuration {
|
||||
|
||||
}
|
||||
|
||||
interface Configuration3 extends Configuration {
|
||||
|
||||
}
|
||||
|
||||
interface Configuration4 extends Configuration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.web.server.servlet.jetty;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.eclipse.jetty.util.resource.PathResourceFactory;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link LoaderHidingResource}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class LoaderHidingResourceTests {
|
||||
|
||||
@Test
|
||||
void listHidesLoaderResources(@TempDir File temp) throws IOException {
|
||||
URI warUri = createExampleWar(temp);
|
||||
Resource resource = new PathResourceFactory().newResource(warUri);
|
||||
LoaderHidingResource loaderHidingResource = new LoaderHidingResource(resource, resource);
|
||||
assertThat(deepList(loaderHidingResource)).hasOnlyElementsOfType(LoaderHidingResource.class)
|
||||
.extracting(Resource::getName)
|
||||
.contains("/assets/image.jpg")
|
||||
.doesNotContain("/org/springframework/boot/Loader.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllResourcesHidesLoaderResources(@TempDir File temp) throws IOException {
|
||||
URI warUri = createExampleWar(temp);
|
||||
Resource resource = new PathResourceFactory().newResource(warUri);
|
||||
LoaderHidingResource loaderHidingResource = new LoaderHidingResource(resource, resource);
|
||||
Collection<Resource> allResources = loaderHidingResource.getAllResources();
|
||||
assertThat(allResources).hasOnlyElementsOfType(LoaderHidingResource.class)
|
||||
.extracting(Resource::getName)
|
||||
.contains("/assets/image.jpg")
|
||||
.doesNotContain("/org/springframework/boot/Loader.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveHidesLoaderResources(@TempDir File temp) throws IOException {
|
||||
URI warUri = createExampleWar(temp);
|
||||
Resource resource = new PathResourceFactory().newResource(warUri);
|
||||
LoaderHidingResource loaderHidingResource = new LoaderHidingResource(resource, resource);
|
||||
assertThat(loaderHidingResource.resolve("/assets/image.jpg").exists()).isTrue();
|
||||
assertThat(loaderHidingResource.resolve("/assets/image.jpg")).isInstanceOf(LoaderHidingResource.class);
|
||||
assertThat(loaderHidingResource.resolve("/assets/non-existent.jpg").exists()).isFalse();
|
||||
assertThat(loaderHidingResource.resolve("/assets/non-existent.jpg")).isInstanceOf(LoaderHidingResource.class);
|
||||
assertThat(loaderHidingResource.resolve("/org/springframework/boot/Loader.class")).isNull();
|
||||
}
|
||||
|
||||
private URI createExampleWar(File temp) throws IOException {
|
||||
File exampleWarFile = new File(temp, "example.war");
|
||||
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(exampleWarFile))) {
|
||||
out.putNextEntry(new ZipEntry("org/"));
|
||||
out.putNextEntry(new ZipEntry("org/springframework/"));
|
||||
out.putNextEntry(new ZipEntry("org/springframework/boot/"));
|
||||
out.putNextEntry(new ZipEntry("org/springframework/boot/Loader.class"));
|
||||
out.putNextEntry(new ZipEntry("assets/"));
|
||||
out.putNextEntry(new ZipEntry("assets/image.jpg"));
|
||||
}
|
||||
URI warUri = URI.create("jar:" + exampleWarFile.toURI() + "!/");
|
||||
FileSystems.newFileSystem(warUri, Collections.emptyMap());
|
||||
return warUri;
|
||||
}
|
||||
|
||||
private List<Resource> deepList(Resource resource) {
|
||||
List<Resource> all = new ArrayList<>();
|
||||
for (Resource listed : resource.list()) {
|
||||
all.add(listed);
|
||||
all.addAll(deepList(listed));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user