EmissaryServer.java

package emissary.server;

import emissary.admin.Startup;
import emissary.client.EmissaryClient;
import emissary.client.EmissaryResponse;
import emissary.client.HTTPConnectionFactory;
import emissary.command.ServerCommand;
import emissary.config.ConfigUtil;
import emissary.config.Configurator;
import emissary.core.EmissaryException;
import emissary.core.EmissaryRuntimeException;
import emissary.core.IPausable;
import emissary.core.MetricsManager;
import emissary.core.Namespace;
import emissary.core.NamespaceException;
import emissary.core.ResourceWatcher;
import emissary.core.sentinel.Sentinel;
import emissary.directory.DirectoryPlace;
import emissary.directory.EmissaryNode;
import emissary.place.IServiceProviderPlace;
import emissary.pool.AgentPool;
import emissary.pool.MoveSpool;
import emissary.roll.RollManager;
import emissary.server.mvc.ThreadDumpAction;
import emissary.server.mvc.ThreadDumpAction.ThreadDumpInfo;
import emissary.spi.SPILoader;

import ch.qos.logback.classic.ViewStatusMessagesServlet;
import com.google.common.annotations.VisibleForTesting;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
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.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.CsrfProtectionFilter;
import org.glassfish.jersey.server.mvc.mustache.MustacheMvcFeature;
import org.glassfish.jersey.servlet.ServletContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.naming.directory.AttributeInUseException;

public class EmissaryServer {

    /* Default namespace name */
    public static final String DEFAULT_NAMESPACE_NAME = "EmissaryServer";

    // Our logger
    private static final Logger LOG = LoggerFactory.getLogger(EmissaryServer.class);

    // Our namespace
    @Nullable
    private String nameSpaceName = null;

    private Server server;

    private final ServerCommand cmd;

    private final EmissaryNode emissaryNode;

    @SuppressWarnings("NonFinalStaticField")
    private static EmissaryServer emissaryServer;

    private EmissaryServer(ServerCommand cmd) {
        this.cmd = cmd;
        this.emissaryNode = new EmissaryNode(cmd.getMode());
    }

    // there should be a better way to set a custom peer.cfg than this
    private EmissaryServer(ServerCommand cmd, EmissaryNode node) {
        this.cmd = cmd;
        this.emissaryNode = node;
    }

    public static EmissaryServer init(ServerCommand cmd) {
        emissaryServer = new EmissaryServer(cmd);
        return emissaryServer;
    }

    // there should be a better way to set a custom peer.cfg than this
    public static EmissaryServer init(ServerCommand cmd, EmissaryNode node) {
        emissaryServer = new EmissaryServer(cmd, node);
        return emissaryServer;
    }

    public static boolean isInitialized() {
        return emissaryServer != null;
    }

    public static EmissaryServer getInstance() {
        if (emissaryServer == null) {
            throw new AssertionError("EmissaryServer has not yet been instantiated!");
        }
        return emissaryServer;
    }

    public EmissaryNode getNode() {
        return this.emissaryNode;
    }

    public ServerCommand getServerCommand() {
        return cmd;
    }

    /**
     * Creates and starts a server that is bound into the local Namespace using DEFAULT_NAMESPACE_NAME and returned
     *
     *
     */
    public Server startServer() {
        // do what StartJetty and then JettyServer did to start
        try {
            // Resource.setDefaultUseCaches(false);

            // needs to be loaded first into the server as it setups up Emissary stuff
            ContextHandler emissaryHandler = buildEmissaryHandler();
            // TODO: rework this, no need for it be set with a context path but if this
            // is left out, it matches / and nothing works correctly
            emissaryHandler.setContextPath("/idontreallyservecontentnowdoi");
            ContextHandler lbConfigHandler = buildLogbackConfigHandler();
            lbConfigHandler.setContextPath("/lbConfig");
            ContextHandler apiHandler = buildApiHandler();
            apiHandler.setContextPath("/api");
            ContextHandler mvcHandler = buildMvcHandler();
            mvcHandler.setContextPath("/emissary");
            // needs to be loaded last into the server so other contexts can match or fall through
            ContextHandler staticHandler = buildStaticHandler();
            staticHandler.setContextPath("/");

            LoginService loginService = buildLoginService();
            ConstraintSecurityHandler security = buildSecurityHandler();
            security.setLoginService(loginService);

            // secure some of the contexts
            final HandlerList securedHandlers = new HandlerList();
            securedHandlers.addHandler(lbConfigHandler);
            securedHandlers.addHandler(apiHandler);
            securedHandlers.addHandler(mvcHandler);
            securedHandlers.addHandler(staticHandler);
            security.setHandler(securedHandlers);

            final HandlerList handlers = new HandlerList();
            handlers.addHandler(emissaryHandler); // not secured, no endpoints and must be loaded first
            handlers.addHandler(security);

            Server configuredServer = configureServer();
            configuredServer.setHandler(handlers);
            configuredServer.addBean(loginService);
            configuredServer.setStopAtShutdown(true);
            configuredServer.setStopTimeout(10000L);
            if (this.cmd.shouldDumpJettyBeans() && LOG.isInfoEnabled()) {
                LOG.info(configuredServer.dump());
            }
            this.server = configuredServer;
            bindServer(); // emissary specific

            configuredServer.start();
            // server.join(); // don't join so we can shutdown

            String serverLocation = cmd.getScheme() + "://" + cmd.getHost() + ":" + cmd.getPort();

            // write out env.sh file here
            Path envsh = Paths.get(ConfigUtil.getProjectBase() + File.separator + "env.sh");
            if (Files.exists(envsh)) {
                LOG.debug("Removing old {}", envsh.toAbsolutePath());
                Files.delete(envsh);
            }
            String envUri = serverLocation + "/api/env.sh";
            EmissaryResponse er = new EmissaryClient().send(new HttpGet(envUri));
            String envString = er.getContentString();
            Files.createFile(envsh);
            Files.write(envsh, envString.getBytes());
            LOG.info("Wrote {}", envsh.toAbsolutePath());
            LOG.debug(" with \n{}", envString);

            if (cmd.isPause()) {
                pause(true);
            } else {
                unpause(true);
            }

            LOG.info("Started EmissaryServer at {}", serverLocation);

            // check if invisible place start-ups occurred on strict server start-up, and shut down server if so.
            if (Startup.isInvisPlacesStartedInStrictMode() && this.server.isStarted()) {
                EmissaryServer.stopServer(true);
                LOG.info("Server shut down due to invisible place startups on strict-mode: {}", Startup.getInvisPlaces());
            }

            return configuredServer;
        } catch (Throwable t) {
            String errorMsg = "Emissary server didn't start";
            throw new EmissaryRuntimeException(errorMsg, t);
        }
    }

    /**
     * Check is server is running
     *
     * @return true if running
     */
    public boolean isServerRunning() {
        return (this.server != null) && this.server.isStarted();
    }


    /**
     * Pause the server
     *
     * @throws NamespaceException if there is an issue
     */
    public static void pause() throws NamespaceException {
        pause(false);
    }

    /**
     * Pause the server
     *
     * @param silent true to silence {@link NamespaceException}, false otherwise
     * @throws NamespaceException if there is an issue
     */
    public static void pause(boolean silent) throws NamespaceException {
        LOG.debug("Pausing Emissary Server");
        Namespace.lookup(IPausable.class, silent).forEach(IPausable::pause);
    }

    /**
     * Unpause the server
     *
     * @throws NamespaceException if there is an issue
     */
    public static void unpause() throws NamespaceException {
        unpause(false);
    }

    /**
     * Unpause the server
     *
     * @param silent true to silence {@link NamespaceException}, false otherwise
     * @throws NamespaceException if there is an issue
     */
    public static void unpause(boolean silent) throws NamespaceException {
        LOG.debug("Unpausing Emissary Server");
        Namespace.lookup(IPausable.class, silent).forEach(IPausable::unpause);
    }

    /**
     * Stop the server
     */
    public static void stopServer() {
        stopServer(false);
    }

    /**
     * Stop the server running under the default name
     *
     * @param force force shutdown
     * @param quiet be quiet about failures if true
     */
    @Deprecated
    public static void stopServer(final boolean force, final boolean quiet) {
        stopServer(getDefaultNamespaceName(), force, quiet);
    }

    /**
     * Stop the server if it is running and remove it from the namespace
     *
     * @param name the namespace name of the server
     * @param quiet be quiet about failures if true
     */
    @Deprecated
    public static void stopServer(final String name, final boolean quiet) {
        stopServer(name, false, quiet);
    }

    /**
     * Stop the server if it is running and remove it from the namespace
     *
     * @param name the namespace name of the server
     * @param force force shutdown
     * @param quiet be quiet about failures if true
     */
    @Deprecated
    public static void stopServer(final String name, final boolean force, final boolean quiet) {
        stopServer(force);
    }

    /**
     * Stop the server with an optional force flag
     *
     * @param force force shutdown
     */
    public static void stopServer(final boolean force) {
        // TODO pull these out to methods and test them

        LOG.info("Beginning shutdown of EmissaryServer");
        logThreadDump("Thread dump before anything");

        try {
            pause();
            LOG.info("Done pausing server");
        } catch (Exception ex) {
            LOG.error("Error pausing server", ex);
        }

        try {
            Sentinel sentinel = Sentinel.lookup();
            sentinel.quit();
        } catch (Exception ex) {
            LOG.warn("No sentinel available");
        }

        try {
            if (force) {
                AgentPool.lookup().kill();
            } else {
                AgentPool.lookup().close();
            }
        } catch (Exception e) {
            LOG.warn("Problem stopping AgentPool", e);
        }

        logThreadDump("Thread dump after closing agent pool");

        try {
            DirectoryPlace.lookup().shutDown();
        } catch (Exception e) {
            LOG.warn("Problem shutting down DirectoryPlace", e);
        }

        try {
            MoveSpool.lookup().quit();
        } catch (Exception e) {
            LOG.warn("Problem stopping MoveSpool", e);
        }

        // Stop the places
        for (String key : Namespace.keySet()) {
            try {
                Object obj = Namespace.lookup(key);
                if (obj instanceof IServiceProviderPlace) {
                    LOG.info("Stopping {} ", obj);
                    ((IServiceProviderPlace) obj).shutDown();
                    // make sure key is removed from namespace
                    Namespace.unbind(key);
                    LOG.info("Done stopping place: {}", key);
                }
            } catch (Exception ex) {
                LOG.error("Error shutting down " + key, ex);
            }
        }
        LOG.info("Done stopping all places");

        RollManager.shutdown();

        // Print the stats
        try {
            ResourceWatcher rw = ResourceWatcher.lookup();
            rw.logStats(LOG);
            rw.quit();
        } catch (Exception ex) {
            LOG.warn("No resource statistics available");
        }

        try {
            MetricsManager.lookup().shutdown();
        } catch (Exception ex) {
            LOG.warn("No metrics manager available");
        }

        SPILoader.unload();

        LOG.info("Done stopping all services");

        // thread dump now
        logThreadDump("Thread dump before stopping jetty server");

        try {
            EmissaryServer.getInstance().getServer().stop();
        } catch (InterruptedException e) {
            LOG.warn("Interrupted! Expected?");
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            LOG.warn("Unable to stop EmissaryServer", e);
        }

        LOG.debug("Unbinding name: {}", getDefaultNamespaceName());
        Namespace.unbind(getDefaultNamespaceName());
        Namespace.clear();
        LOG.info("EmissaryServer completely stopped");
    }

    /**
     * Forcibly stop the server running under the default name
     */
    public static void stopServerForce() {
        stopServer(true);
    }

    @SuppressWarnings("unchecked")
    private static void logThreadDump(String initialLog) {
        if (LOG.isTraceEnabled()) {
            ThreadDumpAction tda = new ThreadDumpAction();
            Map<String, Object> dumps = tda.getThreaddumps();
            StringBuilder sb = new StringBuilder();
            sb.append("\n" + initialLog);
            sb.append("\nThread DUMP");
            sb.append("\nEmissary Version: " + dumps.get("emissary.version"));
            sb.append("\nJVM Version: " + dumps.get("java.version"));
            sb.append("\nJVM Name: " + dumps.get("java.name"));
            Map<String, Object> threadcount = (Map<String, Object>) dumps.get("threadcount");
            sb.append("\nThread count: current=" + threadcount.get("current") + " max=" + threadcount.get("max") + " daemon="
                    + threadcount.get("daemon"));
            sb.append("\nDeadlocked Threads:");
            for (ThreadDumpInfo tdi : (Set<ThreadDumpInfo>) dumps.get("deadlocks")) {
                sb.append("\n" + tdi.stack);
            }
            sb.append("\nThread dump:");
            for (ThreadDumpInfo tdi : (Set<ThreadDumpInfo>) dumps.get("threads")) {
                sb.append("\n" + tdi.stack);
            }
            LOG.trace(sb.toString());
        }
    }

    /**
     * Determine if the emissary node is idle
     */
    public boolean isIdle() {
        try {
            AgentPool pool = AgentPool.lookup();
            return pool.getNumActive() == 0;
        } catch (NamespaceException ex) {
            // empty catch block
        }
        return true;
    }

    /**
     * Non-static way to stop a server
     */
    public void stop() {
        stopServer(false);
    }

    /**
     * Get the reference to the server
     */
    public Server getServer() {
        return this.server;
    }

    @Deprecated
    public synchronized String getNamespaceName() {
        if (this.nameSpaceName == null) {
            this.nameSpaceName = getDefaultNamespaceName();
        }
        return this.nameSpaceName;
    }

    @Deprecated
    public static String getDefaultNamespaceName() {
        return DEFAULT_NAMESPACE_NAME;
    }

    /**
     * Check if server is running
     *
     * @return true if it is in the namespace and is started
     * @deprecated use {@link #isServerRunning()}
     */
    @Deprecated
    public static boolean isStarted() {
        return isStarted(getDefaultNamespaceName());
    }

    /**
     * Check if server is running
     *
     * @param name the namespace name to use as a key
     * @return true if it is in the namespace and is started
     */
    @Deprecated
    public static boolean isStarted(final String name) {
        boolean started = false;
        try {
            final Server s = lookup(name).getServer();
            if (s != null && s.isStarted()) {
                started = true;
            } else {
                LOG.debug("Server found but not started, name={}", name);
            }
        } catch (NamespaceException ex) {
            LOG.debug("No server found using name={}", name);
        }
        return started;
    }

    @Deprecated
    public static boolean exists() {
        try {
            EmissaryServer.lookup();
            return true;
        } catch (NamespaceException ex) {
            // expected
        }
        return false;
    }

    @Deprecated
    public static EmissaryServer lookup() throws NamespaceException {
        return lookup(getDefaultNamespaceName());
    }

    /**
     * Retrieve instance from namespace using default name
     */
    @Deprecated
    public static EmissaryServer lookup(final String name) throws NamespaceException {
        return (EmissaryServer) Namespace.lookup(name);
    }

    private static ConstraintSecurityHandler buildSecurityHandler() {
        ConstraintSecurityHandler handler = new ConstraintSecurityHandler();

        Constraint authConstraint = new Constraint();
        authConstraint.setName("auth");
        authConstraint.setAuthenticate(true);
        authConstraint.setRoles(new String[] {"everyone", "emissary", "admin", "support", "manager"});

        Constraint noAuthConstraint = new Constraint();
        noAuthConstraint.setName("no_auth");
        noAuthConstraint.setAuthenticate(false);

        ConstraintMapping mapping = new ConstraintMapping();
        mapping.setPathSpec("/*");
        mapping.setConstraint(authConstraint);

        ConstraintMapping health = new ConstraintMapping();
        health.setPathSpec("/api/health");
        health.setConstraint(noAuthConstraint);

        handler.setConstraintMappings(new ConstraintMapping[] {mapping, health});
        handler.setAuthenticator(new DigestAuthenticator());
        return handler;
    }

    private static LoginService buildLoginService() {
        String jettyUsersFile = ConfigUtil.getConfigFile("jetty-users.properties");
        System.setProperty("emissary.jetty.users.file", jettyUsersFile); // for EmissaryClient
        return new HashLoginService("EmissaryRealm", jettyUsersFile);
    }

    private void bindServer() throws AttributeInUseException {
        if (Namespace.exists(getDefaultNamespaceName())) {
            LOG.error("EmissaryServer already bound to namespace. This should NEVER happen.");
            throw new AttributeInUseException("EmissaryServer was already bound to the namespace using serverName: " + DEFAULT_NAMESPACE_NAME);
        }

        LOG.debug("Binding {} ", DEFAULT_NAMESPACE_NAME);
        Namespace.bind(getDefaultNamespaceName(), this);
    }

    private ContextHandler buildStaticHandler() {
        // Add pathspec for static assets
        ServletHolder homeHolder = new ServletHolder("static-holder", DefaultServlet.class);
        // If no filesytem path was passed in, load assets from the classpath
        if (null == cmd.getStaticDir()) {
            LOG.debug("Loading static resources from the classpath");
            homeHolder.setInitParameter("resourceBase", this.getClass().getClassLoader().getResource("public").toExternalForm());
        } else {
            // use --staticDir ${project_loc}/src/main/resources/public as command args
            LOG.debug("Loading static resources from staticPath: {}", cmd.getStaticDir());
            homeHolder.setInitParameter("resourceBase", cmd.getStaticDir().toAbsolutePath().toString());
        }
        homeHolder.setInitParameter("dirAllowed", "true");
        homeHolder.setInitParameter("pathInfoOnly", "true");
        // homeHolder.setInitOrder(0);

        ServletContextHandler homeContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
        homeContextHandler.addServlet(homeHolder, "/*");

        return homeContextHandler;
    }

    private ContextHandler buildApiHandler() {

        final ResourceConfig application = new ResourceConfig();
        application.setApplicationName("api");
        application.register(MultiPartFeature.class);
        // setup rest endpoint
        application.packages("emissary.server.api").register(JacksonFeature.class);
        csrfFilter(application);

        ServletHolder apiHolder = new ServletHolder(new ServletContainer(application));

        ServletContextHandler apiHolderContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
        apiHolderContext.addServlet(apiHolder, "/*");

        return apiHolderContext;
    }

    private ContextHandler buildMvcHandler() {

        final ResourceConfig application = new ResourceConfig();
        application.setApplicationName("mvc");
        application.register(MultiPartFeature.class);
        // setup mustache templates
        application.property(MustacheMvcFeature.TEMPLATE_BASE_PATH, "/templates");
        application.register(MustacheMvcFeature.class).packages("emissary.server.mvc");
        csrfFilter(application);

        ServletHolder mvcHolder = new ServletHolder(new ServletContainer(application));
        // mvcHolder.setInitOrder(1);

        ServletContextHandler mvcHolderContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
        mvcHolderContext.addServlet(mvcHolder, "/*");

        return mvcHolderContext;
    }

    protected void csrfFilter(ResourceConfig application) {
        if (this.cmd.isCsrf()) {
            LOG.debug("Enabling csrf protection filter for {}", application.getApplicationName());
            application.register(CsrfProtectionFilter.class);
        } else {
            LOG.debug("Disabling csrf protection filter for {}", application.getApplicationName());
        }
    }

    private ContextHandler buildEmissaryHandler() throws EmissaryException {
        // must set these set or you are not an EmissaryNode
        String configDir = System.getProperty(ConfigUtil.CONFIG_DIR_PROPERTY, null);
        if (configDir == null || !Files.exists(Paths.get(configDir))) {
            throw new EmissaryException("Config dir error. " + ConfigUtil.CONFIG_DIR_PROPERTY + " is " + configDir);
        }
        // set number of agents if it has been set
        if (cmd.getAgents() != 0) {
            System.setProperty("agent.poolsize", Integer.toString(cmd.getAgents()));
        }

        ServletContextHandler emissaryHolderContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
        emissaryHolderContext.addEventListener(new InitializeContext(emissaryNode));

        return emissaryHolderContext;
    }

    private static ContextHandler buildLogbackConfigHandler() {
        ServletHolder lbHolder = new ServletHolder("logback-config-holder", ViewStatusMessagesServlet.class);
        ServletContextHandler lbHolderContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
        lbHolderContext.addServlet(lbHolder, "/*");

        return lbHolderContext;
    }

    @VisibleForTesting
    protected Server configureServer() throws IOException {
        int maxThreads = 250;
        int minThreads = 10;
        int lowThreads = 50;
        int threadsPriority = 9;
        int idleTimeout = (int) TimeUnit.MINUTES.toMillis(15);

        QueuedThreadPool threadPool = new QueuedThreadPool(maxThreads, minThreads, idleTimeout);
        threadPool.setLowThreadsThreshold(lowThreads);
        threadPool.setThreadsPriority(threadsPriority);

        Server configuredServer = new Server(threadPool);
        configuredServer.setConnectors(new Connector[] {
                createServerConnector(configuredServer)
        });
        return configuredServer;
    }

    /**
     * Create a server connector, insecure (http) or secure (https) depending on {@link ServerCommand#isSslEnabled()}
     *
     * @param server the Jetty HTTP Servlet Server
     * @return server connector that is the primary connector for the Jetty server over TCP/IP
     * @throws IOException if there is an error
     */
    private ServerConnector createServerConnector(Server server) throws IOException {
        ServerConnector connector = cmd.isSslEnabled() ? createHttpsConnector(server) : createHttpConnector(server);
        connector.setHost(cmd.getHost());
        connector.setPort(cmd.getPort());
        return connector;
    }

    /**
     * Create an insecure http connector
     *
     * @param server the Jetty HTTP Servlet Server
     * @return server connector that is the primary connector for the Jetty server over TCP/IP
     */
    private static ServerConnector createHttpConnector(Server server) {
        return new ServerConnector(server);
    }

    /**
     * Create a secure https connector
     *
     * @param server the Jetty HTTP Servlet Server
     * @return ServerConnector is the primary connector for the Jetty server over TCP/IP
     * @throws IOException if there is an error
     */
    private ServerConnector createHttpsConnector(Server server) throws IOException {
        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSecureScheme("https");
        httpConfig.setSecurePort(cmd.getPort());

        HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
        httpsConfig.addCustomizer(createSecureRequestCustomizer());

        return new ServerConnector(server,
                new SslConnectionFactory(getSslContextFactory(), HttpVersion.HTTP_1_1.asString()),
                new HttpConnectionFactory(httpsConfig));
    }

    /**
     * SecureRequestCustomizer extracts the attribute from an SSLContext and sets them on the request according to Servlet
     * Specification Requirements. Jetty defaults for the SecureRequestCustomizer are:
     * <ul>
     * <li>isSniRequired() => false // Server Name Indication (SNI) is required
     * <li>isSniHostCheck() => true // SNI Host name must match when there is an SNI certificate
     * <li>getStsMaxAge() => -1 (no max age) // Strict-Transport-Security (STS) max age
     * <li>isStsIncludeSubDomains() => false // include-subdomain property is sent with any STS header
     * </ul>
     *
     * @return a secure request customizer
     */
    private SecureRequestCustomizer createSecureRequestCustomizer() {
        SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer();
        secureRequestCustomizer.setSniHostCheck(cmd.isSniHostCheckEnabled());
        return secureRequestCustomizer;
    }

    /**
     * Create a {@link SslContextFactory.Server} using keystore and truststore config from HttpConnectionFactory.cfg,
     * probably flavored with '-SSL'. Note: only jks files are supported, but could be expanded
     *
     * @return SslContextFactory that is used to configure SSL parameters to be used by server connectors
     * @throws IOException if there is an error getting the context factory
     */
    private static SslContextFactory.Server getSslContextFactory() throws IOException {
        Configurator httpConnFactCfg = ConfigUtil.getConfigInfo(HTTPConnectionFactory.class);
        String keystore = httpConnFactCfg.findStringEntry("javax.net.ssl.keyStore", "no-keystore");
        System.setProperty("javax.net.ssl.keyStore", keystore);
        String keystorePass = httpConnFactCfg.findStringEntry("javax.net.ssl.keyStorePassword", "no-keypass");
        System.setProperty("javax.net.ssl.keyStorePassword", keystorePass);
        String trustStore = httpConnFactCfg.findStringEntry("javax.net.ssl.trustStore", keystore);
        System.setProperty("javax.net.ssl.trustStore", trustStore);
        String trustStorePass = httpConnFactCfg.findStringEntry("javax.net.ssl.trustStorePassword", keystorePass);
        System.setProperty("javax.net.ssl.trustStorePassword", trustStorePass);

        SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
        sslContextFactory.setKeyStorePath(keystore);
        sslContextFactory.setKeyStorePassword(keystorePass);

        KeyStore trustStoreInstance;
        try (InputStream is = Files.newInputStream(Paths.get(trustStore))) {
            trustStoreInstance = KeyStore.getInstance("JKS");
            trustStoreInstance.load(is, trustStorePass.toCharArray());
        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException e) {
            throw new IOException("There was an issue loading the truststore", e);
        }

        sslContextFactory.setTrustStore(trustStoreInstance);
        return sslContextFactory;
    }

}