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.PlainServerConnector;
import eu.unicore.util.jetty.SecuredServerConnector;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.rewrite.handler.HeaderPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.rewrite.handler.Rule;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.AbstractHandlerContainer;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.server.session.DefaultSessionIdManager;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.eclipse.jetty.servlets.DoSFilter;
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.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 pl.edu.icm.unity.exceptions.EngineException;
import pl.edu.icm.unity.exceptions.WrongArgumentException;
import javax.servlet.DispatcherType;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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 UnityServerConfiguration cfg;
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.securityConfiguration = pkiManagement.getMainAuthnAndTrust();
this.listenUrls = listenUrlsProvider.getListenUrls();
this.serverSettings = cfg.getJettyProperties();
this.cfg = cfg;
initServer();
dosFilter = createDoSFilterInstance();
addRedirectHandler(cfg);
}
@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);
}
}
@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 PlainServerConnector(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);
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 AbstractHandlerContainer configureGzip(AbstractHandlerContainer 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()
{
String webContentsDir = cfg.getValue(UnityServerConfiguration.DEFAULT_WEB_CONTENT_PATH);
theServer.setErrorHandler(new JettyErrorHandler(webContentsDir));
}
@Override
public boolean isRunning()
{
return (theServer == null) ? false : theServer.isRunning();
}
private synchronized void initRootHandler()
{
usedContextPaths = new HashMap<>();
mainContextHandler = new ContextHandlerCollection();
deployedEndpoints = new ArrayList<>(16);
}
private void addRedirectHandler(UnityServerConfiguration cfg) throws ConfigurationException
{
if (cfg.isSet(UnityServerConfiguration.DEFAULT_WEB_PATH))
{
try
{
deployHandler(new RedirectHandler(cfg.getValue(
UnityServerConfiguration.DEFAULT_WEB_PATH)), "sys:redirect");
} catch (EngineException e)
{
log.error("Cannot deploy redirect handler " + e.getMessage(), e);
}
}
}
/**
* Deploys a classic Unity endpoint.
*/
@Override
public synchronized void deployEndpoint(WebAppEndpointInstance endpoint)
throws EngineException
{
ServletContextHandler handler = endpoint.getServletContextHandler();
deployHandler(handler, endpoint.getEndpointDescription().getName());
deployedEndpoints.add(endpoint);
}
/**
* Deploys a simple handler. It is only checked if the context path is free.
*/
@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);
Handler wrappedHandler = applyClientIPDiscoveryHandler(handler, endpointId);
mainContextHandler.addHandler(wrappedHandler);
if(theServer.isStarted())
{
try
{
wrappedHandler.start();
} catch (Exception e)
{
mainContextHandler.removeHandler(wrappedHandler);
throw new EngineException("Can not start handler", e);
}
}
usedContextPaths.put(contextPath, wrappedHandler);
}
@Override
public synchronized void undeployAllHandlers() throws EngineException
{
for (Handler handler : usedContextPaths.values())
{
try
{
handler.stop();
} catch (Exception e)
{
throw new EngineException("Can not stop handler", e);
}
}
usedContextPaths.clear();
for (Handler handler : mainContextHandler.getHandlers().clone())
{
mainContextHandler.removeHandler(handler);
}
}
@Override
public synchronized void undeployHandler(String contextPath) throws EngineException
{
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
{
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();
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 FilterHolder createDoSFilterInstance()
{
if (!serverSettings.getBooleanValue(UnityHttpServerConfiguration.ENABLE_DOS_FILTER))
return null;
FilterHolder holder = new FilterHolder(new 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(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();
FilterConfig config = new FilterConfig()
{
@Override
public 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);
}
FilterHolder filterHolder = new FilterHolder(cors);
handler.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD));
}
private ClientIPSettingHandler applyClientIPDiscoveryHandler(AbstractHandlerContainer 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;
}
}