pl.edu.icm.unity.engine.server.JettyServer Maven / Gradle / Ivy
/*
* Copyright (c) 2013 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package pl.edu.icm.unity.engine.server;
import eu.unicore.security.canl.IAuthnAndTrustConfiguration;
import eu.unicore.util.configuration.ConfigurationException;
import eu.unicore.util.jetty.SecuredServerConnector;
import jakarta.servlet.ServletException;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlets.CrossOriginFilter;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.UriCompliance.Violation;
import org.eclipse.jetty.rewrite.handler.HeaderPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.session.DefaultSessionIdManager;
import org.eclipse.jetty.session.SessionIdManager;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.Lifecycle;
import org.springframework.stereotype.Component;
import pl.edu.icm.unity.base.exceptions.EngineException;
import pl.edu.icm.unity.base.exceptions.WrongArgumentException;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.PKIManagement;
import pl.edu.icm.unity.engine.api.config.UnityHttpServerConfiguration;
import pl.edu.icm.unity.engine.api.config.UnityServerConfiguration;
import pl.edu.icm.unity.engine.api.endpoint.WebAppEndpointInstance;
import pl.edu.icm.unity.engine.api.server.NetworkServer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import static pl.edu.icm.unity.engine.api.config.UnityHttpServerConfiguration.*;
/**
* Manages HTTP server. Mostly responsible for creating proper hierarchy of HTTP handlers for deployed
* {@link WebAppEndpointInstance} instances.
*
* Jetty structure which is used:
* {@link ContextHandlerCollection} is used to manage all deployed contexts (fixed, one instance)
* Endpoints provide a single {@link ServletContextHandler} which describes an endpoint's web application.
*
* If needed it is wrapped in some rewrite handler.
* @author K. Benedyczak
*/
@Component
public class JettyServer implements Lifecycle, NetworkServer
{
private static final Logger log = Log.getLogger(Log.U_SERVER_CORE, UnityApplication.class);
private List deployedEndpoints;
private Map usedContextPaths;
private ContextHandlerCollection mainContextHandler;
private FilterHolder dosFilter = null;
private final String defaultWebContentsPath;
private final URL[] listenUrls;
private final IAuthnAndTrustConfiguration securityConfiguration;
private final UnityHttpServerConfiguration serverSettings;
private Server theServer;
@Autowired
public JettyServer(UnityServerConfiguration cfg, PKIManagement pkiManagement,
ListeningUrlsProvider listenUrlsProvider)
{
this(cfg.getJettyProperties(),
cfg.getValue(UnityServerConfiguration.DEFAULT_WEB_CONTENT_PATH),
cfg.getValue(UnityServerConfiguration.DEFAULT_WEB_PATH),
pkiManagement.getMainAuthnAndTrust(),
listenUrlsProvider.getListenUrls());
}
JettyServer(UnityHttpServerConfiguration serverSettings,
String defaultWebContentsPath,
String defaultWebPath,
IAuthnAndTrustConfiguration securityConfiguration,
URL[] listenUrls)
{
this.securityConfiguration = securityConfiguration;
this.listenUrls = listenUrls;
this.serverSettings = serverSettings;
this.defaultWebContentsPath = defaultWebContentsPath;
initServer();
dosFilter = createDoSFilterInstance();
addRedirectHandler(defaultWebPath);
}
@Override
public void start()
{
try
{
log.debug("Starting Jetty HTTP server");
theServer.start();
updatePortsIfNeeded();
log.info("Jetty HTTP server was started");
} catch (Exception e)
{
log.error("Problem starting HTTP Jetty server: " + e.getMessage(), e);
}
}
private void addRedirectHandler(String defaultWebPath) throws ConfigurationException
{
if (defaultWebPath != null && !defaultWebPath.isEmpty())
{
try
{
deployHandler(new RedirectHandler(defaultWebPath), "sys:redirect");
} catch (EngineException e)
{
log.error("Cannot deploy redirect handler " + e.getMessage(), e);
}
}
}
@Override
public void stop()
{
try
{
log.debug("Stopping Jetty HTTP server");
theServer.stop();
log.info("Jetty HTTP server was stopped");
} catch (Exception e)
{
log.error("Problem stopping HTTP Jetty server: " + e.getMessage(), e);
}
}
/**
* Invoked after server is started: updates the listen URLs with the actual port,
* if originally it was set to 0, what means that server should choose a random one
*/
private void updatePortsIfNeeded()
{
Connector[] conns = theServer.getConnectors();
for (int i=0; i 1)
factory.setExcludeCipherSuites(disabledCiphers.split("[ ]+"));
}
log.info("SSL protocol was set to: '" + factory.getProtocol() + "'");
return ssl;
}
/**
* @return an instance of insecure connector. It is only configured not
* to send server version and supports connections logging.
*/
private ServerConnector getPlainConnectorInstance()
{
HttpConnectionFactory httpConnFactory = getHttpConnectionFactory();
return new ServerConnector(theServer, httpConnFactory);
}
/**
* By default http connection factory is configured not to send server identification data.
*/
private HttpConnectionFactory getHttpConnectionFactory()
{
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSendServerVersion(false);
httpConfig.setSendXPoweredBy(false);
httpConfig.setUriCompliance(new UriCompliance("allowForEncodedPathSeparators",
Set.of(Violation.AMBIGUOUS_PATH_SEPARATOR)));
SecureRequestCustomizer src = new SecureRequestCustomizer();
src.setSniHostCheck(serverSettings.getBooleanValue(SNI_HOSTNAME_CHECK));
httpConfig.addCustomizer(src);
return new HttpConnectionFactory(httpConfig);
}
/**
* Creates an insecure connector and configures it.
*/
private ServerConnector createPlainConnector(URL url)
{
log.info("Creating plain HTTP connector on: " + url);
return getPlainConnectorInstance();
}
/**
* Sets parameters on the Connector, which are shared by all of them regardless of their type.
* The default implementation sets port and hostname.
*/
private void configureConnector(ServerConnector connector, URL url) throws ConfigurationException
{
connector.setHost(url.getHost());
connector.setPort(url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
connector.setIdleTimeout(serverSettings.getIntValue(UnityHttpServerConfiguration.MAX_IDLE_TIME));
}
/**
* Configures Gzip filter if gzipping is enabled, for all servlet
* handlers which are configured. Warning: if you use a complex setup of
* handlers it might be better to override this method and enable
* compression selectively.
*/
private Handler configureGzip(Handler handler) throws ConfigurationException
{
boolean enableGzip = serverSettings.getBooleanValue(UnityHttpServerConfiguration.ENABLE_GZIP);
if (enableGzip)
{
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.setMinGzipSize(serverSettings.getIntValue(UnityHttpServerConfiguration.MIN_GZIP_SIZE));
log.info("Enabling GZIP compression filter");
gzipHandler.setServer(theServer);
gzipHandler.setHandler(handler);
return gzipHandler;
} else
return handler;
}
/**
* @return array of URLs where the server is listening
*/
public URL[] getUrls()
{
return listenUrls;
}
private void configureErrorHandler()
{
theServer.setErrorHandler(new JettyErrorHandler(defaultWebContentsPath));
}
@Override
public boolean isRunning()
{
return (theServer == null) ? false : theServer.isRunning();
}
private synchronized void initRootHandler()
{
usedContextPaths = new HashMap<>();
mainContextHandler = new ContextHandlerCollection();
deployedEndpoints = new ArrayList<>(16);
}
/**
* Deploys a classic Unity endpoint.
*/
@Override
public synchronized void deployEndpoint(WebAppEndpointInstance endpoint)
throws EngineException
{
org.eclipse.jetty.ee10.servlet.ServletContextHandler handler = endpoint.getServletContextHandler();
handler.getServletHandler().setDecodeAmbiguousURIs(true);
deployHandler(handler, endpoint.getEndpointDescription().getName());
deployedEndpoints.add(endpoint);
}
@Override
public synchronized void deployHandler(ServletContextHandler handler, String endpointId)
throws EngineException
{
String contextPath = handler.getContextPath();
if (usedContextPaths.containsKey(contextPath))
{
throw new WrongArgumentException("There are (at least) two web " +
"applications configured at the same context path: " + contextPath);
}
addDoSFilter(handler);
addCORSFilter(handler);
ClientIPSettingHandler clientIPSettingHandler = applyClientIPDiscoveryHandler(handler, endpointId);
mainContextHandler.addHandler(clientIPSettingHandler);
if(theServer.isStarted())
{
try
{
clientIPSettingHandler.start();
} catch (Exception e)
{
mainContextHandler.removeHandler(clientIPSettingHandler);
throw new EngineException("Can not start handler", e);
}
}
usedContextPaths.put(contextPath, clientIPSettingHandler);
}
@Override
public synchronized void undeployAllHandlers() throws EngineException
{
for (org.eclipse.jetty.server.Handler handler : usedContextPaths.values())
{
try
{
handler.stop();
} catch (Exception e)
{
throw new EngineException("Can not stop handler", e);
}
}
usedContextPaths.clear();
for (org.eclipse.jetty.server.Handler handler : List.copyOf(mainContextHandler.getHandlers()))
{
mainContextHandler.removeHandler(handler);
}
}
@Override
public synchronized void undeployHandler(String contextPath) throws EngineException
{
org.eclipse.jetty.server.Handler handler = usedContextPaths.get(contextPath);
try
{
handler.stop();
} catch (Exception e)
{
throw new EngineException("Can not stop handler", e);
}
mainContextHandler.removeHandler(handler);
usedContextPaths.remove(contextPath);
}
@Override
public synchronized void undeployEndpoint(String id) throws EngineException
{
if(deployedEndpoints.stream().anyMatch(endp -> endp.getEndpointDescription().getName().equals(id)))
undeployEE10Endpoint(id);
else
throw new WrongArgumentException("There is no deployed endpoint with id " + id);
}
public void undeployEE10Endpoint(String id) throws EngineException
{
WebAppEndpointInstance endpoint = null;
for (WebAppEndpointInstance endp: deployedEndpoints)
{
if (endp.getEndpointDescription().getName().equals(id))
{
endpoint = endp;
break;
}
}
if (endpoint == null)
throw new WrongArgumentException("There is no deployed endpoint with id " + id);
String contextPath = endpoint.getEndpointDescription().getEndpoint().getContextAddress();
org.eclipse.jetty.server.Handler handler = usedContextPaths.get(contextPath);
try
{
handler.stop();
} catch (Exception e)
{
throw new EngineException("Can not stop handler", e);
}
mainContextHandler.removeHandler(handler);
usedContextPaths.remove(contextPath);
deployedEndpoints.remove(endpoint);
}
@Override
public Set getUsedContextPaths()
{
return usedContextPaths.keySet();
}
private org.eclipse.jetty.ee10.servlet.FilterHolder createDoSFilterInstance()
{
if (!serverSettings.getBooleanValue(UnityHttpServerConfiguration.ENABLE_DOS_FILTER))
return null;
org.eclipse.jetty.ee10.servlet.FilterHolder holder = new org.eclipse.jetty.ee10.servlet.FilterHolder(new org.eclipse.jetty.ee10.servlets.DoSFilter());
Set keys = serverSettings.getSortedStringKeys(UnityHttpServerConfiguration.DOS_FILTER_PFX);
for (String key: keys)
holder.setInitParameter(key.substring(
UnityHttpServerConfiguration.PREFIX.length() +
UnityHttpServerConfiguration.DOS_FILTER_PFX.length()),
serverSettings.getProperty(key));
return holder;
}
private void addDoSFilter(ServletContextHandler handler)
{
if (dosFilter != null)
{
log.info("Enabling DoS filter on context " + handler.getContextPath());
handler.addFilter(dosFilter, "/*", EnumSet.of(jakarta.servlet.DispatcherType.REQUEST));
}
}
private void addCORSFilter(ServletContextHandler handler)
{
boolean enable = serverSettings.getBooleanValue(UnityHttpServerConfiguration.ENABLE_CORS);
if (!enable)
return;
log.info("Enabling CORS");
CrossOriginFilter cors = new CrossOriginFilter();
jakarta.servlet.FilterConfig config = new jakarta.servlet.FilterConfig()
{
@Override
public jakarta.servlet.ServletContext getServletContext()
{
throw new UnsupportedOperationException("Not implemented");
}
@Override
public Enumeration getInitParameterNames()
{
throw new UnsupportedOperationException("Not implemented");
}
@Override
public String getInitParameter(String name)
{
return serverSettings.getValue(UnityHttpServerConfiguration.CORS_PFX + name);
}
@Override
public String getFilterName()
{
return "CORS";
}
};
try
{
cors.init(config);
} catch (ServletException e)
{
throw new ConfigurationException("Error setting up CORS", e);
}
org.eclipse.jetty.ee10.servlet.FilterHolder filterHolder = new org.eclipse.jetty.ee10.servlet.FilterHolder(cors);
handler.addFilter(filterHolder, "/*", EnumSet.of(jakarta.servlet.DispatcherType.REQUEST, jakarta.servlet.DispatcherType.FORWARD));
}
private ClientIPSettingHandler applyClientIPDiscoveryHandler(Handler baseHandler, String endpointId)
{
ClientIPDiscovery ipDiscovery = new ClientIPDiscovery(serverSettings.getIntValue(PROXY_COUNT),
serverSettings.getBooleanValue(ALLOW_NOT_PROXIED_TRAFFIC));
IPValidator ipValidator = new IPValidator(
serverSettings.getListOfValues(ALLOWED_IMMEDIATE_CLIENTS));
log.info("Enabling client IP discovery filter");
ClientIPSettingHandler handler = new ClientIPSettingHandler(ipDiscovery, ipValidator, endpointId);
handler.setServer(theServer);
handler.setHandler(baseHandler);
return handler;
}
}