JettyHTTPServerEngine.java
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cxf.transport.http_jetty;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.BufferOverflowException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import org.apache.cxf.Bus;
import org.apache.cxf.common.i18n.Message;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.common.util.ReflectionUtil;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.common.util.SystemPropertyAction;
import org.apache.cxf.configuration.jsse.SSLContextServerParameters;
import org.apache.cxf.configuration.jsse.SSLUtils;
import org.apache.cxf.configuration.jsse.TLSServerParameters;
import org.apache.cxf.configuration.security.ClientAuthentication;
import org.apache.cxf.helpers.JavaUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.transport.HttpUriMapper;
import org.apache.cxf.transport.http.HttpServerEngineSupport;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.ee10.servlet.ServletHandler.Default404Servlet;
import org.eclipse.jetty.ee10.servlet.ServletHandler.MappedServlet;
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.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields.Mutable;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes.Type;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.io.ByteBufferOutputStream;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.Retainable;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
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.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.Container;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
/**
* This class is the Jetty HTTP Server Engine that is configured to
* work off of a designated port. The port will be enabled for
* "http" or "https" depending upon its successful configuration.
*/
public class JettyHTTPServerEngine implements ServerEngine, HttpServerEngineSupport {
public static final String DO_NOT_CHECK_URL_PROP = "org.apache.cxf.transports.http_jetty.DontCheckUrl";
private static final Logger LOG = LogUtils.getL7dLogger(JettyHTTPServerEngine.class);
/**
* This is the network port for which this engine is allocated.
*/
private int port;
/**
* This is the network address for which this engine is allocated.
*/
private String host;
/**
* This field holds the protocol for which this engine is
* enabled, i.e. "http" or "https".
*/
private String protocol = "http";
private Boolean isSessionSupport = false;
private int sessionTimeout = -1;
private Boolean isReuseAddress = true;
private Boolean continuationsEnabled = true;
private int maxIdleTime = 200000;
private Boolean sendServerVersion = true;
private int servantCount;
private Server server;
private Connector connector;
private List<Handler> handlers;
private Map<String, ServletContextHandler> contextHandlerMap = new HashMap<String, ServletContextHandler>();
private ContextHandlerCollection contexts;
private Container.Listener mBeanContainer;
private SessionHandler sessionHandler;
private ThreadPool threadPool;
/**
* This field holds the TLS ServerParameters that are programatically
* configured. The tlsServerParamers (due to JAXB) holds the struct
* placed by SpringConfig.
*/
private TLSServerParameters tlsServerParameters;
/**
* This field hold the threading parameters for this particular engine.
*/
private ThreadingParameters threadingParameters;
/**
* This boolean signfies that SpringConfig is over. finalizeConfig
* has been called.
*/
private boolean configFinalized;
private List<String> registedPaths = new CopyOnWriteArrayList<>();
/**
* This constructor is called by the JettyHTTPServerEngineFactory.
*/
public JettyHTTPServerEngine(Container.Listener mBeanContainer, String host, int port) {
this.host = host;
this.port = port;
this.mBeanContainer = mBeanContainer;
}
public JettyHTTPServerEngine() {
}
public void setThreadPool(ThreadPool p) {
threadPool = p;
}
public void setPort(int p) {
port = p;
}
public void setHost(String host) {
this.host = host;
}
public void setContinuationsEnabled(boolean enabled) {
continuationsEnabled = enabled;
}
public boolean getContinuationsEnabled() {
return continuationsEnabled;
}
/**
* Returns the protocol "http" or "https" for which this engine
* was configured.
*/
public String getProtocol() {
return protocol;
}
/**
* Returns the port number for which this server engine was configured.
* @return
*/
public int getPort() {
return port;
}
/**
* Returns the host for which this server engine was configured.
* @return
*/
public String getHost() {
return host;
}
/**
* This method will shut down the server engine and
* remove it from the factory's cache.
*/
public void shutdown() {
registedPaths.clear();
if (shouldDestroyPort()) {
if (servantCount == 0) {
JettyHTTPServerEngineFactory.destroyForPort(port);
} else {
LOG.log(Level.WARNING, "FAILED_TO_SHUTDOWN_ENGINE_MSG", port);
}
}
}
private boolean shouldDestroyPort() {
//if we shutdown the port, on SOME OS's/JVM's, if a client
//in the same jvm had been talking to it at some point and keep alives
//are on, then the port is held open for about 60 seconds
//afterwards and if we restart, connections will then
//get sent into the old stuff where there are
//no longer any servant registered. They pretty much just hang.
//this is most often seen in our unit/system tests that
//test things in the same VM.
String s = SystemPropertyAction
.getPropertyOrNull("org.apache.cxf.transports.http_jetty.DontClosePort." + port);
if (s == null) {
s = SystemPropertyAction
.getPropertyOrNull("org.apache.cxf.transports.http_jetty.DontClosePort");
}
return !Boolean.valueOf(s);
}
private boolean shouldCheckUrl(Bus bus) {
Object prop = null;
if (bus != null) {
prop = bus.getProperty(DO_NOT_CHECK_URL_PROP);
}
if (prop == null) {
prop = SystemPropertyAction.getPropertyOrNull(DO_NOT_CHECK_URL_PROP);
}
return !PropertyUtils.isTrue(prop);
}
/**
* get the jetty server instance
* @return
*/
public Server getServer() {
return server;
}
/**
* Set the jetty server instance
* @param s
*/
public void setServer(Server s) {
server = s;
}
/**
* set the jetty server's connector
* @param c
*/
public void setConnector(Connector c) {
connector = c;
}
/**
* set the jetty server's handlers
* @param h
*/
public void setHandlers(List<Handler> h) {
handlers = h;
}
public void setSessionSupport(boolean support) {
isSessionSupport = support;
}
public boolean isSessionSupport() {
return isSessionSupport;
}
public List<Handler> getHandlers() {
return handlers;
}
public Connector getConnector() {
return connector;
}
public boolean isReuseAddress() {
return isReuseAddress;
}
public void setReuseAddress(boolean reuse) {
isReuseAddress = reuse;
}
public int getMaxIdleTime() {
return maxIdleTime;
}
public void setMaxIdleTime(int maxIdle) {
maxIdleTime = maxIdle;
}
protected void checkRegistedContext(URL url) {
String path = url.getPath();
for (String registedPath : registedPaths) {
if (path.equals(registedPath)) {
throw new Fault(new Message("ADD_HANDLER_CONTEXT_IS_USED_MSG", LOG, url, registedPath));
}
// There are some context path conflicts which could cause the JettyHTTPServerEngine
// doesn't route the message to the right JettyHTTPHandler
if (path.equals(HttpUriMapper.getContextName(registedPath))) {
throw new Fault(new Message("ADD_HANDLER_CONTEXT_IS_USED_MSG", LOG, url, registedPath));
}
if (registedPath.equals(HttpUriMapper.getContextName(path))) {
throw new Fault(new Message("ADD_HANDLER_CONTEXT_CONFILICT_MSG", LOG, url, registedPath));
}
}
}
private Server createServer() {
Server s = null;
if (connector != null && connector.getServer() != null) {
s = connector.getServer();
}
if (threadPool != null) {
try {
if (s == null) {
s = new Server(threadPool);
} else {
s.addBean(threadPool);
}
} catch (Exception e) {
//ignore
}
}
if (s == null) {
s = new Server();
}
// need an error handler that won't leak information about the exception
// back to the client.
ErrorHandler eh = new CxfJettyErrorHandler();
s.setErrorHandler(eh);
return s;
}
/**
* Register a servant.
*
* @param url the URL associated with the servant
* @param handler notified on incoming HTTP requests
*/
//CHECKSTYLE:OFF
public synchronized void addServant(URL url, JettyHTTPHandler handler) {
//CHECKSTYLE:ON
if (shouldCheckUrl(handler.getBus())) {
checkRegistedContext(url);
}
initializeContexts();
SecurityHandler securityHandler = null;
if (server == null) {
DefaultHandler defaultHandler = null;
// create a new jetty server instance if there is no server there
server = createServer();
addServerMBean();
if (connector == null) {
connector = createConnector(getHost(), getPort(), handler.getBus());
if (LOG.isLoggable(Level.FINER)) {
logConnector((ServerConnector)connector);
}
}
server.addConnector(connector);
setupThreadPool();
/*
* The server may have no handler, it might have a collection handler,
* it might have a one-shot. We need to add one or more of ours.
*
*/
int numberOfHandlers = 1;
if (handlers != null) {
numberOfHandlers += handlers.size();
}
Handler existingHandler = server.getHandler();
Handler.Collection handlerCollection = null;
boolean existingHandlerCollection = existingHandler instanceof Handler.Collection;
if (existingHandlerCollection) {
handlerCollection = (Handler.Collection) existingHandler;
}
if (!existingHandlerCollection
&&
(existingHandler != null || numberOfHandlers > 1)) {
handlerCollection = new Handler.Sequence(new ArrayList<Handler>());
if (existingHandler != null) {
handlerCollection.addHandler(existingHandler);
}
server.setHandler(handlerCollection);
}
/*
* At this point, the server's handler is a collection. It was either
* one to start, or it is now one containing only the single handler
* that was there to begin with.
*/
if (handlers != null && !handlers.isEmpty()) {
for (Handler h : handlers) {
// Filtering out the jetty default handler
// which should not be added at this point.
if (h instanceof DefaultHandler) {
defaultHandler = (DefaultHandler) h;
} else {
if (h instanceof SecurityHandler
&& ((SecurityHandler)h).getHandler() == null) {
//if h is SecurityHandler(such as ConstraintSecurityHandler)
//then it need be on top of JettyHTTPHandler
//set JettyHTTPHandler as inner handler if
//inner handler is null
securityHandler = (SecurityHandler)h;
} else {
if (!(h instanceof JettyHTTPHandler)) {
//JettyHTTPHandler is a ServletHandler
//and must be added with ServletContext Handler later
handlerCollection.addHandler(h);
} else {
String contextName = HttpUriMapper.getContextName(url.getPath());
ServletContextHandler context = null;
context = ((JettyHTTPHandler)h).createContextHandler();
context.setContextPath(contextName);
context.setHandler(h);
contexts.addHandler(context);
}
}
}
}
}
/*
* handlerCollection may be null here if is only one handler to deal with.
* Which in turn implies that there can't be a 'defaultHander' to deal with.
*/
if (handlerCollection != null) {
if (defaultHandler != null) {
handlerCollection.addHandler(defaultHandler);
}
} else {
server.setHandler(contexts);
}
try {
server.start();
} catch (Exception e) {
LOG.log(Level.SEVERE, "START_UP_SERVER_FAILED_MSG", new Object[] {e.getMessage(), port});
//problem starting server
try {
server.stop();
server.destroy();
} catch (Exception ex) {
//ignore - probably wasn't fully started anyway
}
server = null;
throw new Fault(new Message("START_UP_SERVER_FAILED_MSG", LOG, e.getMessage(), port), e);
}
}
String contextName = HttpUriMapper.getContextName(url.getPath());
if (contextName.length() == 0) {
contextName = "/"; //ensure it is mapped as root context,
}
String path = HttpUriMapper.getResourceBase(url.getPath());
if (!path.equals("/")) {
//add /* to enable wild match
path = path + "/*";
}
if (contextName.endsWith("/*")) {
//This is how Jetty ServletContextHandler handle the contextName
//without suffix "/", the "*" suffix will be removed
contextName = contextName + "/";
}
ServletContextHandler context = null;
if (this.contextHandlerMap.containsKey(contextName)
&& this.contextHandlerMap.get(contextName).getServletHandler().getMatchedServlet(path) != null) {
context = this.contextHandlerMap.get(contextName);
ServletHandler servletHandler = context.getServletHandler();
MatchedResource<MappedServlet> mappedServlet = servletHandler.getMatchedServlet(path);
ServletHolder servletHolder = mappedServlet.getResource().getServletHolder();
Servlet servlet = null;
try {
servlet = servletHolder.getServlet();
} catch (ServletException ex) {
LOG.log(Level.WARNING, "ADD_HANDLER_FAILED_MSG", new Object[] {
ex.getMessage()
});
}
if (servlet != null && servlet instanceof JettyHTTPHandler && servletHolder.isStarted()) {
try {
// the servlet exist with the same path
// just update the servlet
context.stop();
servletHolder.setServlet(handler);
servletHolder.stop();
servletHolder.start();
servletHolder.initialize();
context.start();
} catch (Exception ex) {
LOG.log(Level.WARNING, "ADD_HANDLER_FAILED_MSG", new Object[] {
ex.getMessage()
});
}
} else {
try {
context.addServlet(handler, path);
} catch (Exception ex) {
LOG.log(Level.WARNING, "ADD_HANDLER_FAILED_MSG", new Object[] {
ex.getMessage()
});
}
}
} else {
context = handler.createContextHandler();
context.setContextPath(contextName);
context.addServlet(handler, path);
contexts.addHandler(context);
this.contextHandlerMap.put(contextName, context);
// bind the jetty http handler with the context handler
if (isSessionSupport) {
SessionHandler sh = configureSession();
if (securityHandler != null) {
//use the securityHander which already wrap the jetty http handler
sh.setHandler(securityHandler);
}
context.setSessionHandler(sh);
} else {
// otherwise, just the one.
if (securityHandler != null) {
//use the securityHander which already wrap the jetty http handler
context.setSecurityHandler(securityHandler);
}
}
}
if (server.getHandler() != contexts) {
for (Handler h : contexts.getHandlers()) {
((Handler.Collection)server.getHandler()).addHandler(h);
try {
h.start();
} catch (Exception ex) {
LOG.log(Level.WARNING, "ADD_HANDLER_FAILED_MSG", new Object[] {ex.getMessage()});
}
}
}
ServletContext sc = context.getServletContext();
handler.setServletContext(sc);
String smap = getHandlerName(url, context);
handler.setName(smap);
if (contexts.isStarted() && context != null && !context.isStarted()) {
try {
context.start();
} catch (Exception ex) {
LOG.log(Level.WARNING, "ADD_HANDLER_FAILED_MSG", new Object[] {ex.getMessage()});
}
}
registedPaths.add(url.getPath());
++servantCount;
}
private SessionHandler configureSession() {
// If we have sessions, we need two handlers.
SessionHandler sh = null;
try {
if (Server.getVersion().startsWith("9.2") || Server.getVersion().startsWith("9.3")) {
if (sessionHandler == null) {
sessionHandler = new SessionHandler();
}
sh = new SessionHandler();
Method get = ReflectionUtil.getDeclaredMethod(SessionHandler.class, "getSessionManager");
Method set = ReflectionUtil.getDeclaredMethod(SessionHandler.class, "setSessionManager",
get.getReturnType());
if (this.getSessionTimeout() >= 0) {
Method setMaxInactiveInterval = ReflectionUtil
.getDeclaredMethod(get.getReturnType(), "setMaxInactiveInterval", int.class);
ReflectionUtil.setAccessible(setMaxInactiveInterval)
.invoke(ReflectionUtil.setAccessible(get).invoke(sessionHandler), 20);
}
ReflectionUtil.setAccessible(set)
.invoke(sh, ReflectionUtil.setAccessible(get).invoke(sessionHandler));
} else {
// 9.4+ stores the session id handling and cache and everything on the server, just need
// the handler
sh = new SessionHandler();
if (this.getSessionTimeout() >= 0) {
Method setMaxInactiveInterval = ReflectionUtil
.getDeclaredMethod(SessionHandler.class, "setMaxInactiveInterval", int.class);
ReflectionUtil.setAccessible(setMaxInactiveInterval).invoke(sh, 20);
}
}
} catch (Throwable t) {
}
return sh;
}
private String getHandlerName(URL url, ServletContextHandler context) {
String contextPath = context.getContextPath();
String path = url.getPath();
if (path.startsWith(contextPath)) {
if ("/".equals(contextPath)) {
return path;
}
return path.substring(contextPath.length());
} else {
return HttpUriMapper.getResourceBase(url.getPath());
}
}
private void initializeContexts() {
if (contexts == null) {
contexts = new ContextHandlerCollection();
if (server != null) {
if (server.getHandler() instanceof ContextHandlerCollection) {
contexts = (ContextHandlerCollection) server.getHandler();
} else {
server.setHandler(contexts);
}
}
}
}
private void addServerMBean() {
if (mBeanContainer == null) {
return;
}
try {
Container container = getContainer(server);
container.addEventListener(mBeanContainer);
mBeanContainer.beanAdded(null, server);
} catch (RuntimeException rex) {
throw rex;
} catch (Exception r) {
throw new RuntimeException(r);
}
}
private void removeServerMBean() {
try {
mBeanContainer.beanRemoved(null, server);
} catch (RuntimeException rex) {
throw rex;
} catch (Exception r) {
throw new RuntimeException(r);
}
}
private Connector createConnector(String hosto, int porto, final Bus bus) {
// now we just use the SelectChannelConnector as the default connector
SslContextFactory.Server sslcf = null;
if (tlsServerParameters != null) {
sslcf = new SslContextFactory.Server() {
protected void doStart() throws Exception {
setSslContext(createSSLContext(this));
super.doStart();
checkKeyStore();
}
public void checkKeyStore() {
//we'll handle this later
}
};
decorateCXFJettySslSocketConnector(sslcf);
}
int major = 9;
int minor = 0;
try {
String[] version = Server.getVersion().split("\\.");
major = Integer.parseInt(version[0]);
minor = Integer.parseInt(version[1]);
} catch (Exception e) {
// unparsable version
}
ServerConnector result = (ServerConnector)createConnectorJetty(sslcf, hosto, porto, major, minor, bus);
try {
result.setPort(porto);
if (hosto != null) {
result.setHost(hosto);
}
result.setReuseAddress(isReuseAddress());
} catch (RuntimeException rex) {
throw rex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
return result;
}
AbstractConnector createConnectorJetty(SslContextFactory.Server sslcf, String hosto, int porto,
int major, int minor, final Bus bus) {
final AbstractConnector result;
try {
HttpConfiguration httpConfig = new HttpConfiguration();
/**
* LEGACY compliance mode that models Jetty-9.4 behavior
* by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT},
* {@link Violation#AMBIGUOUS_EMPTY_SEGMENT}, {@link Violation#AMBIGUOUS_PATH_SEPARATOR},
* {@link Violation#AMBIGUOUS_PATH_ENCODING}
* and {@link Violation#UTF16_ENCODINGS}.
*/
httpConfig.setUriCompliance(UriCompliance.LEGACY);
httpConfig.setSendServerVersion(getSendServerVersion());
HttpConnectionFactory httpFactory = new HttpConnectionFactory(httpConfig);
Collection<ConnectionFactory> connectionFactories = new ArrayList<>();
result = new org.eclipse.jetty.server.ServerConnector(server);
if (tlsServerParameters != null) {
httpConfig.addCustomizer(new SecureRequestCustomizer(tlsServerParameters.isSniHostCheck()));
if (isHttp2Enabled(bus)) {
try {
// The ALPN processors are application specific (as per Jetty docs) and are pluggable as
// additional dependency.
final ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
alpn.setDefaultProtocol(httpFactory.getProtocol());
final SslConnectionFactory scf = new SslConnectionFactory(sslcf, alpn.getProtocol());
connectionFactories.add(scf);
connectionFactories.add(alpn);
connectionFactories.add(new HTTP2ServerConnectionFactory(httpConfig));
} catch (Throwable ex) {
if (isHttp2Required(bus)) {
throw ex;
}
}
}
if (connectionFactories.isEmpty()) {
final SslConnectionFactory scf = new SslConnectionFactory(sslcf, httpFactory.getProtocol());
connectionFactories.add(scf);
}
// Ensure http/1.1 is last in the list so it is the lowest priority in ALPN
connectionFactories.add(httpFactory);
// Has to be set before the default protocol change
result.setConnectionFactories(connectionFactories);
String proto = (major > 9 || (major == 9 && minor >= 3)) ? "SSL" : "SSL-HTTP/1.1";
result.setDefaultProtocol(proto);
} else if (isHttp2Enabled(bus)) {
connectionFactories.add(httpFactory);
try {
connectionFactories.add(new HTTP2CServerConnectionFactory(httpConfig) {
@Override
public Connection upgradeConnection(Connector c, EndPoint endPoint,
org.eclipse.jetty.http.MetaData.Request request,
Mutable response101)
throws BadMessageException {
if (request.getContentLength() > 0
|| request.getHttpFields().contains("Transfer-Encoding")) {
// if there is a body, we cannot upgrade
return null;
}
return super.upgradeConnection(c, endPoint, request, response101);
}
});
} catch (Throwable ex) {
if (isHttp2Required(bus)) {
throw ex;
}
}
result.setConnectionFactories(connectionFactories);
}
if (getMaxIdleTime() > 0) {
result.setIdleTimeout(Long.valueOf(getMaxIdleTime()));
}
} catch (RuntimeException rex) {
throw rex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
return result;
}
protected SSLContext createSSLContext(SslContextFactory scf) throws Exception {
// The full SSL context is provided by SSLContextServerParameters
if (tlsServerParameters instanceof SSLContextServerParameters sslContextServerParameters) {
return sslContextServerParameters.getSslContext();
}
String proto = tlsServerParameters.getSecureSocketProtocol() == null
? "TLS" : tlsServerParameters.getSecureSocketProtocol();
// Jetty 9 excludes SSLv3 by default. So if we want it then we need to
// remove it from the default excluded protocols
boolean allowSSLv3 = "SSLv3".equals(proto);
if (allowSSLv3 || !tlsServerParameters.getIncludeProtocols().isEmpty()) {
List<String> excludedProtocols = new ArrayList<>();
for (String excludedProtocol : scf.getExcludeProtocols()) {
if (!(tlsServerParameters.getIncludeProtocols().contains(excludedProtocol)
|| (allowSSLv3 && ("SSLv3".equals(excludedProtocol)
|| "SSLv2Hello".equals(excludedProtocol))))) {
excludedProtocols.add(excludedProtocol);
}
}
String[] revisedProtocols = new String[excludedProtocols.size()];
excludedProtocols.toArray(revisedProtocols);
scf.setExcludeProtocols(revisedProtocols);
}
for (String p : tlsServerParameters.getExcludeProtocols()) {
scf.addExcludeProtocols(p);
}
SSLContext context = tlsServerParameters.getJsseProvider() == null
? SSLContext.getInstance(detectProto(proto, allowSSLv3))
: SSLContext.getInstance(detectProto(proto, allowSSLv3), tlsServerParameters.getJsseProvider());
KeyManager[] keyManagers = tlsServerParameters.getKeyManagers();
KeyManager[] configuredKeyManagers = org.apache.cxf.transport.https.SSLUtils.configureKeyManagersWithCertAlias(
tlsServerParameters, keyManagers);
context.init(configuredKeyManagers,
tlsServerParameters.getTrustManagers(),
tlsServerParameters.getSecureRandom());
// Set the CipherSuites
final String[] supportedCipherSuites =
SSLUtils.getServerSupportedCipherSuites(context);
if (tlsServerParameters.getCipherSuitesFilter() != null
&& tlsServerParameters.getCipherSuitesFilter().isSetExclude()) {
String[] excludedCipherSuites =
SSLUtils.getFilteredCiphersuites(tlsServerParameters.getCipherSuitesFilter(),
supportedCipherSuites,
LOG,
true);
scf.setExcludeCipherSuites(excludedCipherSuites);
}
String[] includedCipherSuites =
SSLUtils.getCiphersuitesToInclude(tlsServerParameters.getCipherSuites(),
tlsServerParameters.getCipherSuitesFilter(),
context.getServerSocketFactory().getDefaultCipherSuites(),
supportedCipherSuites,
LOG);
scf.setIncludeCipherSuites(includedCipherSuites);
return context;
}
protected static String detectProto(String proto, boolean allowSSLv3) {
if (allowSSLv3 && JavaUtils.getJavaMajorVersion() >= 14) {
// Since Java 14, the SSLv3 aliased to TLSv1 (so SSLv3 effectively is not
// supported). To make it work, the custom SSL context has to be created
// instead along with specifying server / client properties as needed, for
// example:
// -Djdk.tls.server.protocols=SSLv3,TLSv1
// -Djdk.tls.client.protocols=SSLv3,TLSv1
return "SSL";
} else {
return proto;
}
}
protected void setClientAuthentication(SslContextFactory.Server con,
ClientAuthentication clientAuth) {
con.setWantClientAuth(true);
if (clientAuth != null) {
if (clientAuth.isSetWant()) {
con.setWantClientAuth(clientAuth.isWant());
}
if (clientAuth.isSetRequired()) {
con.setNeedClientAuth(clientAuth.isRequired());
}
}
}
/**
* This method sets the security properties for the CXF extension
* of the JettySslConnector.
*/
private void decorateCXFJettySslSocketConnector(
SslContextFactory.Server con
) {
setClientAuthentication(con,
tlsServerParameters.getClientAuthentication());
con.setCertAlias(tlsServerParameters.getCertAlias());
// TODO Once we switch to use SslContextFactory.Server instead, we can get rid of this line
con.setEndpointIdentificationAlgorithm(null);
}
private static Container getContainer(Object server) {
if (server instanceof Container) {
return (Container)server;
}
try {
return (Container)server.getClass().getMethod("getContainer").invoke(server);
} catch (RuntimeException t) {
throw t;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
private static void logConnector(ServerConnector connector) {
try {
String h = connector.getHost();
int port = connector.getPort();
LOG.finer("connector.host: " + (h == null ? "null" : "\"" + h + "\""));
LOG.finer("connector.port: " + port);
} catch (Throwable t) {
//ignore
}
}
protected void setupThreadPool() {
if (isSetThreadingParameters()) {
ThreadPool pl = getThreadPool();
//threads for the acceptors and selectors are taken from
//the pool so we need to have room for those
AbstractConnector aconn = (AbstractConnector) connector;
int acc = aconn.getAcceptors() * 2;
if (getThreadingParameters().isSetMaxThreads()
&& getThreadingParameters().getMaxThreads() <= acc) {
throw new Fault(new Message("NOT_ENOUGH_THREADS", LOG,
port,
acc + 1,
getThreadingParameters().getMaxThreads(),
acc));
}
if (!(pl instanceof QueuedThreadPool)) {
throw new Fault(new Message("NOT_A_QUEUED_THREAD_POOL", LOG, pl.getClass()));
}
if (getThreadingParameters().isThreadNamePrefixSet()) {
((QueuedThreadPool) pl).setName(getThreadingParameters().getThreadNamePrefix());
}
if (getThreadingParameters().isSetMinThreads()) {
((QueuedThreadPool) pl).setMinThreads(getThreadingParameters().getMinThreads());
}
if (getThreadingParameters().isSetMaxThreads()) {
((QueuedThreadPool) pl).setMaxThreads(getThreadingParameters().getMaxThreads());
}
}
}
private ThreadPool getThreadPool() {
ThreadPool pool = server.getThreadPool();
if (pool == null) {
pool = new QueuedThreadPool();
try {
server.getClass().getMethod("setThreadPool", ThreadPool.class).invoke(server, pool);
} catch (RuntimeException t) {
throw t;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
return pool;
}
/**
* Remove a previously registered servant.
*
* @param url the URL the servant was registered against.
*/
public synchronized void removeServant(URL url) {
String contextName = HttpUriMapper.getContextName(url.getPath());
if (contextName.length() == 0) {
contextName = "/"; //ensure it is mapped as root context,
}
String smap = HttpUriMapper.getResourceBase(url.getPath());
if (!smap.equals("/")) {
//add /* to enable wild match
smap = smap + "/*";
}
if (contextName.endsWith("/*")) {
//This is how Jetty ServletContextHandler handle the contextName
//without suffix "/", the "*" suffix will be removed
contextName = contextName + "/";
}
boolean found = false;
if (server != null && server.isRunning()) {
for (Handler handler : contexts.getHandlers()) {
if (handler instanceof ServletContextHandler) {
ServletContextHandler contextHandler = (ServletContextHandler) handler;
ServletHandler servletHandler = contextHandler.getServletHandler();
MatchedResource<MappedServlet> mappedServlet = servletHandler.getMatchedServlet(smap);
if (mappedServlet == null) {
continue;
}
ServletHolder servletHolder = mappedServlet.getResource().getServletHolder();
Servlet servlet = null;
try {
servlet = servletHolder.getServlet();
} catch (ServletException ex) {
LOG.log(Level.WARNING, "REMOVE_HANDLER_FAILED_MSG", new Object[] {
ex.getMessage()
});
continue;
}
if (servlet != null && servlet instanceof JettyHTTPHandler
&& (contextName.equals(contextHandler.getContextPath())
|| (StringUtils.isEmpty(contextName)
&& "/".equals(contextHandler.getContextPath())))
&& smap.startsWith(((JettyHTTPHandler)servlet).getName())) {
try {
servletHolder.stop();
contextHandler.getContext().destroy(servlet);
//need to remove path from ServletHandler._servletMappings
//and has to access the private field.
List<?> servletMappings
= ReflectionUtil.accessDeclaredField("_servletMappings",
ServletHandler.class,
servletHandler,
List.class);
ServletMapping servletMapping = servletHandler.getServletMapping(smap);
servletMappings.remove(servletMapping);
boolean hasActiveServlet = false;
for (ServletHolder myHolder : servletHandler.getServlets()) {
if (myHolder.getServlet() != null
&& !(myHolder.getServlet() instanceof Default404Servlet)) {
hasActiveServlet = true;
break;
}
}
if (!hasActiveServlet) {
contexts.removeHandler(handler);
handler.stop();
handler.destroy();
this.contextHandlerMap.remove(contextName);
}
} catch (Exception ex) {
LOG.log(Level.WARNING, "REMOVE_HANDLER_FAILED_MSG",
new Object[] {ex.getMessage()});
}
found = true;
break;
}
}
}
}
if (!found) {
LOG.log(Level.WARNING, "CAN_NOT_FIND_HANDLER_MSG", new Object[]{url});
}
registedPaths.remove(url.getPath());
--servantCount;
}
/**
* Get a registered servant.
*
* @param url the associated URL
* @return the HttpHandler if registered
*/
public synchronized Handler getServant(URL url) {
String contextName = HttpUriMapper.getContextName(url.getPath());
//final String smap = HttpUriMapper.getResourceBase(url.getPath());
Handler ret = null;
// After a stop(), the server is null, and therefore this
// operation should return null.
if (server != null) {
for (Handler handler : server.getDescendants(ServletContextHandler.class)) {
if (handler instanceof ServletContextHandler) {
ServletContextHandler contextHandler = (ServletContextHandler) handler;
if (contextName.equals(contextHandler.getContextPath())) {
ret = contextHandler.getHandler();
break;
}
}
}
}
return ret;
}
/**
* Get a registered context handler.
*
* @param url the associated URL
* @return the HttpHandler if registered
*/
public synchronized ServletContextHandler getContextHandler(URL url) {
String contextName = HttpUriMapper.getContextName(url.getPath());
ServletContextHandler ret = null;
// After a stop(), the server is null, and therefore this
// operation should return null.
if (server != null) {
for (Handler handler : server.getDescendants(ServletContextHandler.class)) {
if (handler instanceof ServletContextHandler) {
ServletContextHandler contextHandler = (ServletContextHandler) handler;
if (contextName.equals(contextHandler.getContextPath())) {
ret = contextHandler;
break;
}
}
}
}
return ret;
}
private boolean isSsl() {
if (connector == null) {
return false;
}
try {
return "https".equalsIgnoreCase(connector.getDefaultConnectionFactory().getProtocol());
} catch (Exception ex) {
//ignore
}
return false;
}
protected void retrieveListenerFactory() {
if (tlsServerParameters != null) {
if (connector != null && !isSsl()) {
LOG.warning("Connector " + connector + " for JettyServerEngine Port "
+ port + " does not support SSL connections.");
return;
}
protocol = "https";
} else {
if (isSsl()) {
throw new RuntimeException("Connector " + connector + " for JettyServerEngine Port "
+ port + " does not support non-SSL connections.");
}
protocol = "http";
}
LOG.fine("Configured port " + port + " for \"" + protocol + "\".");
}
/**
* This method is called after configure on this object.
*/
@PostConstruct
public void finalizeConfig() {
retrieveListenerFactory();
checkConnectorPort();
this.configFinalized = true;
}
private void checkConnectorPort() {
if (null != connector) {
int cp = ((ServerConnector)connector).getPort();
if (port != cp) {
throw new UncheckedIOException(new IOException("Error: Connector port " + cp + " does not match"
+ " with the server engine port " + port));
}
}
}
/**
* This method is called by the ServerEngine Factory to destroy the
* listener.
*
*/
protected void stop() throws Exception {
registedPaths.clear();
if (server != null) {
try {
if (connector != null) {
connector.stop();
if (connector instanceof Closeable) {
((Closeable)connector).close();
}
}
} finally {
if (contexts != null && contexts.getHandlers() != null) {
for (Handler h : contexts.getHandlers()) {
h.stop();
}
contexts.stop();
}
contexts = null;
server.stop();
if (mBeanContainer != null) {
removeServerMBean();
}
// After upgrade Jetty to 11.0.8, server.destroy() will clear all MBeans from container
// The old version doesn't behave like this because MBeanContainer was shareable but
// is not anymore (the factory should create new a container for each server engine).
server.destroy();
server = null;
}
}
}
/**
* This method is used to programmatically set the TLSServerParameters.
* This method may only be called by the factory.
* @throws IOException
*/
public void setTlsServerParameters(TLSServerParameters params) {
tlsServerParameters = params;
if (this.configFinalized) {
this.retrieveListenerFactory();
}
}
/**
* This method returns the programmatically set TLSServerParameters, not
* the TLSServerParametersType, which is the JAXB generated type used
* in SpringConfiguration.
* @return
*/
public TLSServerParameters getTlsServerParameters() {
return tlsServerParameters;
}
/**
* This method sets the threading parameters for this particular
* server engine.
* This method may only be called by the factory.
*/
public void setThreadingParameters(ThreadingParameters params) {
threadingParameters = params;
}
/**
* This method returns whether the threading parameters are set.
*/
public boolean isSetThreadingParameters() {
return threadingParameters != null;
}
/**
* This method returns the threading parameters that have been set.
* This method may return null, if the threading parameters have not
* been set.
*/
public ThreadingParameters getThreadingParameters() {
return threadingParameters;
}
public void setSendServerVersion(Boolean sendServerVersion) {
this.sendServerVersion = sendServerVersion;
}
public Boolean getSendServerVersion() {
return sendServerVersion;
}
public int getSessionTimeout() {
return sessionTimeout;
}
public void setSessionTimeout(int sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
private final class CxfJettyErrorHandler extends ErrorHandler {
public boolean handle(Request request, Response response, Callback callback) throws Exception {
String msg = (String)request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
if (StringUtils.isEmpty(msg) || msg.contains("org.apache.cxf.interceptor.Fault")) {
msg = HttpStatus.getMessage(response.getStatus());
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, msg);
}
if (response instanceof Response) {
((Response)response).setStatus(response.getStatus());
}
return super.handle(request, response, callback);
}
//CHECKSTYLE:OFF
protected boolean generateAcceptableResponse(Request request, Response response,
Callback callback, String contentType,
List<Charset> charsets, int code, String message,
Throwable cause)
//CHECKSTYLE:ON
throws IOException {
Type type;
Charset charset;
switch (contentType) {
case "text/html":
case "text/*":
case "*/*":
type = Type.TEXT_HTML;
charset = charsets.stream().findFirst().orElse(StandardCharsets.ISO_8859_1);
break;
case "text/json":
case "application/json":
if (charsets.contains(StandardCharsets.UTF_8)) {
charset = StandardCharsets.UTF_8;
} else if (charsets.contains(StandardCharsets.ISO_8859_1)) {
charset = StandardCharsets.ISO_8859_1;
} else {
return false;
}
type = Type.TEXT_JSON.is(contentType) ? Type.TEXT_JSON : Type.APPLICATION_JSON;
break;
case "text/plain":
type = Type.TEXT_PLAIN;
charset = charsets.stream().findFirst().orElse(StandardCharsets.ISO_8859_1);
break;
default:
return false;
}
int bufferSize = request.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize();
bufferSize = Math.min(8192, bufferSize); // TODO ?
RetainableByteBuffer buffer = request.getComponents().getByteBufferPool().acquire(bufferSize,
false);
try {
// write into the response aggregate buffer and flush it asynchronously.
// Looping to reduce size if buffer overflows
boolean showStacks = isShowStacks();
while (true) {
try {
buffer.clear();
ByteBufferOutputStream out = new ByteBufferOutputStream(buffer.getByteBuffer());
PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset));
switch (type) {
case TEXT_HTML:
writeErrorHtml(request, writer, charset, code, message, cause, showStacks);
break;
case TEXT_JSON:
case APPLICATION_JSON:
writeErrorJson(request, writer, code, message, cause, showStacks);
break;
case TEXT_PLAIN:
writeErrorPlain(request, writer, code, message, cause, showStacks);
break;
default:
throw new IllegalStateException();
}
writer.flush();
break;
} catch (BufferOverflowException e) {
if (showStacks) {
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER, "Disable stacks for " + e.toString());
}
showStacks = false;
continue;
}
LOG.log(Level.WARNING,
"Error page too large:" + message);
break;
}
}
if (!buffer.hasRemaining()) {
buffer.release();
callback.succeeded();
return true;
}
response.getHeaders().put(type.getContentTypeField(charset));
response.write(true, buffer.getByteBuffer(), new WriteErrorCallback(callback, buffer));
return true;
} catch (Throwable x) {
buffer.release();
throw x;
}
}
class WriteErrorCallback extends Callback.Nested {
private final Retainable retainable;
WriteErrorCallback(Callback callback, Retainable retainable) {
super(callback);
this.retainable = retainable;
}
@Override
public void completed() {
this.retainable.release();
}
}
}
}