# GenericWebServer.java

    /*
    ** Module   : GenericWebServer.java
    ** Abstract : generic design for an embedded web server.
    **
    ** Copyright (c) 2013-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- -------------------------------------Description------------------------------------------
    ** 001 MAG 20131105 First version based on jetty 9.1
    ** 002 MAG 20131129 Import server KeyStore.
    ** 003 AB  20150515 Removed configWsContext() and now directly use the handler collection.
    ** 004 SBI 20160220 Changed to set the websocket parameters from the server provided configuration.
    ** 005 HC  20170612 Changes related to implementation of new GWT-based Admin client.
    ** 006 SBI 20171116 Changed to set SSL context factory to look up its web store first.
    ** 008 CA  20190717 Let Jetty provide the HTTPS, for secure connections.
    **     CA  20190719 Allow listening on both secure and insecure ports.
    ** 009 HC  20190911 Implemented ad-hoc handler registration.
    ** 010 CA  20191122 Allow optional secure port.
    **         20191203 Added APIs to load WAR web apps in distinct Jetty servers.
    ** 011 CA  20200122 Javadoc fixes.
    ** 012 CA  20200915 Upgraded to Jetty 9.4.22.
    **                  Added getWebAppConnectors - used by third-party apps running in the same JVM as FWD.
    ** 014 IAS 20210422 Use daemon thread pool for ScheduledExecutorScheduler in the ServerConnector.
    **     IAS 20210420 Use daemon thread pool for ScheduledExecutorScheduler in the Server.
    **     SBI 20211003 Used HttpConfigurationConstants.
    **     CA  20211103 Do not allow a web server to start in HTTPS mode if SSLContext can't be initialized.
    **     CA  20211214 Renamed the authKeyStore, authKeyStorePassword and authKeyEntryPassword nodes to be the
    **                  same node names as used by AppServerConnectionPool (keyStore, storePassword and
    **                  entryPassword).
    **     VVT 20220316 getWarFile(): now the argument can be an absolute file path.
    **     TJD 20220331 Replaced deprecated new SslContextFactory() with SslContextFactory.Server()
    **     HC  20220427 Added Conscrypt SSL provider support for Web GUI JS Client - Java Client channel.
    **     IAS 20230109 Replace import of 'edu.emory.mathcs.backport.java.util.concurrent.atomic'
    **                  with 'java.util.concurrent.atomic'.
    ** 015 CA  20230509 Either the secure or insecure ports must be configured for a webapp running within the FWD
    **                  server JVM.
    ** 016 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 017 EVL 20230608 Fix from SBI to resolve socket availability issue.
    ** 018 CA  20230630 Isolate FWD jar (p2j.jar) and other dependencies from the webapp, except jars needed by
    **                  the jetty server runtime running this webapp.  This allows the webapp to have different
    **                  versions of the jars used by FWD.
    ** 019 GBB 20230825 SecurityManager context methods calls updated.
    ** 020 CA  20231026 Allow cleanup of HTTP sessions created for 3rd web apps running within FWD server.
    **                  The timeout is set at <webapp>/sessionTimeout, with a default value of 3600 seconds.
    ** 021 CA  20240524 All 'jetty-' prefixed jars are exposed to the .war app.
    ** 022 CA  20240614 Added websocket related jars to be exposed to the .war app.
    ** 023 GBB 20240709 Renamed HttpConfigurationConstants to WebConfigurationConstants.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.web;

    import java.io.*;
    import java.net.*;
    import java.nio.file.Paths;
    import java.security.*;
    import java.util.*;
    import java.util.concurrent.atomic.*;
    import java.util.logging.*;

    import javax.net.ssl.*;
    import javax.servlet.DispatcherType;
    import javax.servlet.http.*;

    import com.goldencode.p2j.util.logging.*;
    import org.conscrypt.*;
    import org.eclipse.jetty.annotations.*;
    import org.eclipse.jetty.http.*;
    import org.eclipse.jetty.plus.webapp.*;
    import org.eclipse.jetty.server.*;
    import org.eclipse.jetty.server.Handler;
    import org.eclipse.jetty.server.Server;
    import org.eclipse.jetty.server.handler.*;
    import org.eclipse.jetty.server.session.*;
    import org.eclipse.jetty.util.ssl.*;
    import org.eclipse.jetty.util.thread.*;
    import org.eclipse.jetty.webapp.*;
    import org.eclipse.jetty.webapp.Configuration;
    import org.eclipse.jetty.websocket.servlet.*;

    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.*;

    /**
     * A generic design for an embedded web server having support for WebSocket's.
     *
     * @author mag
     *
     */
    public class GenericWebServer
    implements WebConfigurationConstants
    {
       /** Logger. */
       protected static final CentralLogger LOG = CentralLogger.get(GenericWebServer.class.getName());

       /** ScheduledExecutorScheduler ID generator */
       private static final AtomicInteger SCHEDULER_ID = new AtomicInteger();

       /** Default session timeout - 3600 seconds - for 3rd party web apps running in the FWD server. */
       private static final int DEFAULT_SESSION_TIMEOUT = 3600;

       /** The common http configuration settings */
       private final HttpConfiguration httpConfiguration;

       /** Exported server key store */
       private ServerKeyStore serverKeyStore;

       /** The bootstrap configuration */
       private BootstrapConfig config;

       /** Embedded jetty server */
       private Server server;

       /** Context Handlers */
       private HandlerCollection handlerCollection = new HandlerCollection(true);

       /** Context path */
       private String contextPath = "/";

       /** Server port */
       private int port;

       /** Server host name */
       private String host;

       /** The thread pool. */
       private QueuedThreadPool pool;

       /** Whether Conscrypt SSL provider will be enabled */
       private boolean enableConscrypt;

       /**
        * Create a new server.
        */
       public GenericWebServer()
       {
          this(false);
       }

       /**
        * Create a new server.
        *
        * @param  enableConscrypt
        *         A flag to enable Conscrypt SSL provider.
        */
       public GenericWebServer(boolean enableConscrypt)
       {
          this.enableConscrypt = enableConscrypt;

          pool = new QueuedThreadPool();
          pool.setDaemon(true);

          httpConfiguration = new HttpConfiguration();

          server = new Server(pool);
          Scheduler scheduler = new ScheduledExecutorScheduler(
                   String.format("Session-HouseKeeper-%x", hashCode()),
                   true
          );
          server.addBean(scheduler);
       }

       /**
        * Resolve the WAR file with the given name - this will be a file relative to the current class
        * location.
        *
        * @param    warName
        *           The WAR file name (including extension).
        *
        * @return   The {@link File} referencing this resource.
        */
       public static File getWarFile(String warName)
       {
          File warFile = new File(warName);
          if (warFile.isAbsolute())
          {
             return warFile;
          }

          URL url = GenericWebServer.class.getProtectionDomain().getCodeSource().getLocation();
          String classPath = null;
          try
          {
             classPath = url.toURI().getPath();
          }
          catch (URISyntaxException e)
          {
             return null;
          }

          File jarFile = new File(classPath);
          if (!jarFile.exists())
          {
             return null;
          }
          // default path
          String defaultPath = jarFile.getParentFile().getPath();

          if (!defaultPath.endsWith(File.separator))
          {
             defaultPath += File.separator;
          }

          return new File(defaultPath + warName);
       }

       /**
        * Get the HTTP and HTTPS connectors to run the specified web application, embedded in the specified Jetty
        * server.
        *
        * @param    name
        *           The web application name.  The directory configuration is found in the <code>[name]WebApp</code>
        *           node - this includes the port, secure port, and host.
        * @param    server
        *           The Jetty server.
        *
        * @return   The secure and insecure HTTP connectors.
        *
        * @throws   Exception
        *           In case of configuration problems.
        */
       public static ServerConnector[] getWebAppConnectors(String name, Server server)
       throws Exception
       {
          String appName = name + "WebApp";

          int insecurePort = Utils.getDirectoryNodeInt(null, appName + "/port", -1, false);
          int securePort = Utils.getDirectoryNodeInt(null, appName + "/secureport", -1, false);
          String host = Utils.getDirectoryNodeString(null, appName + "/host", null, false);
          if (insecurePort == -1 && securePort == -1)
          {
             throw new IllegalStateException("No port was not configured for " + name + " web app.");
          }

          List<ServerConnector> connectors = new ArrayList<>(2);

          if (securePort > 0)
          {

             // String alias = Utils.getDirectoryNodeString(null, appName + "/authAlias", null, false);
             String ksfile = Utils.getDirectoryNodeString(null, appName + "/keyStore", null, false);
             String kspass = Utils.getDirectoryNodeString(null, appName + "/storePassword",
                                                          null, false);
             String kepass = Utils.getDirectoryNodeString(null, appName + "/entryPassword",
                                                          null, false);

             if (ksfile == null || kspass == null || kepass == null)
             {
                throw new IllegalStateException("The " + name +" web app is configured to start, but " +
                                                " the authentication details are not configured.");
             }

             SecureWebServer sslServer = new SecureWebServer(ksfile, kspass, kepass);
             connectors.add(configSslConnector(server,
                                               host,
                                               securePort,
                                               sslServer.getSslContextFactory()));
          }

          // do this always after the secure, so we know the secure connector is first
          if (insecurePort >= 0)
          {
             ServerConnector connector = new ServerConnector(server);
             connector.setHost(host);
             connector.setPort(insecurePort);
             connectors.add(connector);
          }

          return connectors.toArray(new ServerConnector[2]);
       }

       /**
        * Initialize a WEB app, from a specified WAR file.
        * <p>
        * Web application's configuration will be read from the directory, using the
        * <code>name + "WebApp"</code> node, with these settings:
        * <ul>
        *    <li><code>port</code>, with the insecure port.  Optional.</li>
        *    <li><code>secureport</code>, with the secure port. Optional. If secure port is set, then:
        *       <ul>
        *          <li><code>keyStore</code>, the store with the FWD process private-key and
        *              certificate, to authenticate to FWD server.</li>
        *          <li><code>storePassword</code>, the password for the private-key.</li>
        *          <li><code>entryPassword</code>, the password for the certificate.</li>
        *       </ul>
        *    </li>
        *    <li><code>basepath</code>, with the base path serving this web app.</li>
        *    <li><code>warfile</code>, the WAR file name, relative to FWD server's working dir.
        *        Defaults to <code>defaultWar</code></li>
        * </ul>
        *
        * @param    name
        *           The WEB app name.
        * @param    defaultWar
        *           The default WAR file name.
        * @param    servletName
        *           The servlet name serving this WEB app.
        *
        * @return   A {@link GenericWebServer} instance.
        */
       public static GenericWebServer initializeWebApp(String name,
                                                       String defaultWar,
                                                       String servletName)
       {
          String appName = name + "WebApp";

          int port = Utils.getDirectoryNodeInt(null, appName + "/port", -1, false);
          int securePort = Utils.getDirectoryNodeInt(null, appName + "/secureport", -1, false);
          String host = Utils.getDirectoryNodeString(null, appName + "/host", null, false);
          if (port == -1 && securePort == -1)
          {
             throw new IllegalStateException("A secure or insecure port must be configured for " + name + " web app.");
          }

          String basepath = Utils.getDirectoryNodeString(null, appName + "/basepath", "/", false);
          String webAppWar = Utils.getDirectoryNodeString(null, appName + "/warfile", defaultWar, false);

          File warFile = getWarFile(webAppWar);
          if (warFile == null || !warFile.exists())
          {
             throw new RuntimeException("WAR file can not be found: " + warFile.getAbsolutePath());
          }
          WebAppContext webapp = new WebAppContext();
          String javaClassPath = System.getProperty("java.class.path");
          if (javaClassPath != null)
          {
             ClasspathPattern pat = webapp.getServerClasspathPattern();

             String[] dependencies =
             {
                "javax.servlet-api",
                "log4j",
                "tuscany-sdo",
                "jetty-",
                "javax.websocket-",
                "javax-websocket-",
                "websocket-"
             };
             HashSet<String> allow = new HashSet<>(Arrays.asList(dependencies));

             Class<?>[] omit =
             {
                GenericWebServer.class, // p2j.jar
                WebAppContext.class, // jetty
             };

             for (Class<?> cls : omit)
             {
                URL res = cls.getResource(cls.getSimpleName() + ".class");
                String file = res.getFile();
                int idx = file.indexOf("!/");
                if (idx >= 0)
                {
                   file = file.substring(0, idx);
                }
                file = file.substring(file.lastIndexOf(File.separator) + 1);
                allow.add(file);
             }

             // everything in the 'allow' list will be exposed to the webapp; otherwise, the .jar file is isolated
             // from the webapp, allowing it to have a different version.

             l1:
             for (String path : javaClassPath.split(File.pathSeparator))
             {
                try
                {
                   path = Paths.get(path).toRealPath().toString();
                }
                catch (IOException e)
                {
                   LOG.warning("Could not resolve real path:" + path);
                }

                // resolve absolute name of the path, so symlinks do not appear in the path
                for (String tok : allow)
                {
                   if (path.indexOf(tok) >= 0)
                   {
                      continue l1;
                   }
                }
                pat.add("file:" + path);
             }
          }

          try
          {
             // ensure jasperreports_extension.properties is loaded only from the webapp.
             // this is not enough - jasper looks into all parent classloaders, a custom ExtensionsRegistry needs
             // to be implemented by the webapp
             webapp.setClassLoader(new WebAppClassLoader(Thread.currentThread().getContextClassLoader(), webapp)
             {
                @Override
                public Enumeration<URL> getResources(String name)
                throws IOException
                {
                   if (name.contains("jasperreports_extension.properties"))
                   {
                      List<URL> res = new ArrayList<>();
                      Enumeration<URL> urls = this.findResources(name);
                      while(urls != null && urls.hasMoreElements())
                      {
                         URL url = urls.nextElement();
                         res.add(url);
                      }

                      return Collections.enumeration(res);
                   }

                   return super.getResources(name);
                }
             });
          }
          catch (IOException exc)
          {
             throw new RuntimeException(exc);
          }

          webapp.setContextPath(basepath);
          webapp.setWar(warFile.getAbsolutePath());
          webapp.setExtractWAR(true);

          webapp.setConfigurations(new Configuration[]
          {
             new AnnotationConfiguration(),
             new EnvConfiguration(),
             new FragmentConfiguration(),
             new JettyWebXmlConfiguration(),
             new MetaInfConfiguration(),
             new PlusConfiguration(),
             new WebInfConfiguration(),
             new WebXmlConfiguration(),
          });

          webapp.addFilter(WebAuthFilter.class,
                           "/*",
                           EnumSet.of(DispatcherType.ASYNC,
                                      DispatcherType.ERROR,
                                      DispatcherType.FORWARD,
                                      DispatcherType.INCLUDE,
                                      DispatcherType.REQUEST));

          GenericWebServer server;
          if (securePort > 0)
          {

             // String alias = Utils.getDirectoryNodeString(null, appName + "/authAlias", null, false);
             String ksfile = Utils.getDirectoryNodeString(null, appName + "/keyStore", null, false);
             String kspass = Utils.getDirectoryNodeString(null, appName + "/storePassword",
                                                          null, false);
             String kepass = Utils.getDirectoryNodeString(null, appName + "/entryPassword",
                                                          null, false);

             if (ksfile == null || kspass == null || kepass == null)
             {
                throw new IllegalStateException("The " + name +" web app is configured to start, but " +
                                                " the authentication details are not configured.");
             }

             server = new SecureWebServer(ksfile, kspass, kepass);
          }
          else
          {
             server = new GenericWebServer();
          }

          int dirTimeout = Utils.getDirectoryNodeInt(null, appName + "/sessionTimeout", DEFAULT_SESSION_TIMEOUT,
                                                     false);
          // do not allow sessions to live forever
          int timeout = dirTimeout <= 0 ? DEFAULT_SESSION_TIMEOUT : dirTimeout;

          DefaultSessionIdManager idmanager = new DefaultSessionIdManager(server.getServer());
          SessionHandler sessions = new SessionHandler();
          sessions.addEventListener(new SessionCleaner());
          sessions.setSessionIdManager(idmanager);
          webapp.setSessionHandler(sessions);

          HouseKeeper houseKeeper = new HouseKeeper();
          idmanager.setSessionHouseKeeper(houseKeeper);

          server.addHandler(webapp);

          Thread t = new Thread(() ->
          {
             try
             {
                server.getServer().setSessionIdManager(idmanager);
                server.startup(host, securePort, port);

                // these need to be set *after* the server is started
                webapp.getSessionHandler().setMaxInactiveInterval(timeout);
                try
                {
                   houseKeeper.setIntervalSec(timeout);
                }
                catch (Exception e1)
                {
                   e1.printStackTrace();
                }

                server.join();
             }
             catch (Exception e)
             {
                throw new RuntimeException("Error starting up the " + name + " Secure Servlet", e);
             }
          });
          t.setDaemon(true);
          t.setName(servletName);
          t.start();

          return server;
       }

       /**
        * Startup the embedded server.
        * The sever will listen on all interfaces.
        * The port number is dynamically allocated by the OS.
        *
        * @throws  Exception
        *          If an error condition occur during the server startup.
        */
       public void startup()
       throws Exception
       {
          this.startup(null, 0);
       }

       /**
        * Startup server.
        *
        * @param   host
        *          Host name or IP address. If <b>null</b> server will listen on
        *          all interfaces.
        * @param   port
        *          Port number to listen. If <b>0</b> the port is OS dynamically
        *          allocated.
        *
        * @throws  Exception
        *          If an error condition occur during the server startup.
        */
       public void startup(String host, int port)
       throws Exception
       {
          startup(host, port, -1);
       }

       /**
        * Startup server.
        *
        * @param   host
        *          Host name or IP address. If <b>null</b> server will listen on
        *          all interfaces.
        * @param   securePort
        *          Port number to listen securely. If <b>0</b> the port is OS dynamically
        *          allocated.
        * @param   insecurePort
        *          Port number to listen insecurely.  This is optional, set to a positive value to
        *          use it.
        *
        * @throws  Exception
        *          If an error condition occur during the server startup.
        */
       public void startup(String host, int securePort, int insecurePort)
       throws Exception
       {
          // SSL Context Factory for HTTPS
          SslContextFactory sslContextFactory = getSslContextFactory();

          if (sslContextFactory == null && securePort >= 0)
          {
             String shost = (host == null ? "localhost" : host);
             String msg = String.format("Could not initialize the SSL context for %s:%s", shost, securePort);
             throw new RuntimeException(msg);
          }

          server.manage(pool);

          if (securePort >= 0)
          {
             server.addConnector(configSslConnector(server,
                                                    host,
                                                    securePort,
                                                    sslContextFactory,
                                                    httpConfiguration));
          }
          // do this always after the secure, so we know the secure connector is first
          if (insecurePort >= 0)
          {
             ServerConnector connector = new ServerConnector(server);
             connector.setHost(host);
             connector.setPort(insecurePort);
             connector.setReuseAddress(true);
             server.addConnector(connector);
          }

          server.setHandler(handlerCollection);
          server.setStopAtShutdown(true);
          server.start();

          // get host and port after server is starting
          getRuntimeParams();

          LOG.logp(Level.INFO,
                   "GenericWebServer.startup",
                   "",
                   CentralLogger.generate("Server URL: https://%s:%d%s",
                                          getHost(),
                                          getPort(),
                                          contextPath));

          if (securePort >= 0 && sslContextFactory != null)
          {
             String provider = sslContextFactory.getProvider();
             LOG.log(Level.INFO, "SSL provider: " + (provider == null ? "<default>" : provider));
          }
       }

       /**
        * Shutdown server.
        *
        * @throws  Exception
        *          If the server cannot be stopped.
        */
       public void shutdown()
       throws Exception
       {
          if (server != null && server.isRunning())
          {
             server.stop();
          }
       }

       /**
        * Waits for server thread pool to die.
        *
        * @throws  Exception
        *          If any error condition occur.
        */
       public void join()
       throws Exception
       {
          if (server != null && server.isRunning())
          {
             server.join();
          }
       }

       /**
        * Get the {@link #server jetty server} instance.
        *
        * @return   See above.
        */
       public Server getServer()
       {
          return server;
       }

       /**
        * Find out if server is running.
        *
        * @return  <code>true</code> if server is running <code>false</code> otherwise.
        */
       public boolean isRunning()
       {
          return server != null && server.isRunning();
       }

       /**
        * Set the bootstrap configuration structure if any.
        *
        * @param   config
        *          The <code>BootstrapConfig</code> instance or <b>null</b>.
        */
       public void setBootstrapConfig(BootstrapConfig config)
       {
          this.config = config;
       }

       /**
        * Get the server runtime host and port number.
        *
        */
       private void getRuntimeParams()
       {
          if (server.isRunning())
          {
             Connector[] connectors = server.getConnectors();
             ServerConnector connector = (ServerConnector) connectors[0];
             port = connector.getLocalPort();
             host = connector.getHost();
          }
       }

       /**
        * Register a list of handlers.
        *
        * @param   handlers
        *          A list of handlers to be registered.
        */
       public void addHandler(Handler[] handlers)
       {
          for (Handler handler : handlers)
          {
             addHandler(handler);
          }
       }

       /**
        * Register a new handler.
        *
        * @param   handler
        *          A Handler instance.
        */
       public void addHandler(Handler handler)
       {
          if (handler != null)
          {
             handlerCollection.addHandler(handler);
          }
       }

       /**
        * Removes the handler from the list of handlers.
        *
        * @param   handler
        *          The handler to remove.
        */
       public void removeHandler(Handler handler)
       {
          handlerCollection.removeHandler(handler);
       }

       /**
        * Register a new WebSocket.
        *
        * @param   target
        *          The WebSocket target.
        * @param   webSocket
        *          A POJO decorated with <code>@WebSocket</code> annotation.
        */
       public void addWebSocketHandler(String target, Class<?> webSocket)
       {
          Handler wsHandler = new PojoWebSocketHandler(target, webSocket, new WebSocketConfig(config));
          handlerCollection.addHandler(wsHandler);
       }

       /**
        * Register a new WebSocket by hand using a custom WebSocket creator.
        *
        * @param   target
        *          The WebSocket target.
        * @param   creator
        *          A custom <code>WebSocketCreator</code> instance.
        */
       public void addWebSocketHandler(String target, WebSocketCreator creator)
       {
          Handler wsHandler = new PojoWebSocketHandler(target, creator, new WebSocketConfig(config));
          handlerCollection.addHandler(wsHandler);
       }

       /**
        * Returns the common http configuration.
        *
        * @return   The common http configuration
        */
       public HttpConfiguration getHttpConfiguration()
       {
          return httpConfiguration;
       }

       /**
        * Construct a SSL Context Factory for HTTPS.
        *
        * @return  The <code>SslContextFactory</code> instance.
        *
        * @throws  Exception
        *          If any error conditions occur.
        */
       protected SslContextFactory getSslContextFactory()
       throws Exception
       {
          SecurityManager sm = SecurityManager.getInstance();

          SslContextFactory sslContextFactory = new SslContextFactory.Server();

          if (serverKeyStore != null && serverKeyStore.isWebValid())
          {
             // Import sever SSL context
             ByteArrayInputStream stream = new ByteArrayInputStream(serverKeyStore.getWebKeyStore());
             KeyStore keyStore = KeyStore.getInstance("JKS");
             keyStore.load(stream, serverKeyStore.getWebKeyStorePassword().toCharArray());
             sslContextFactory.setKeyStore(keyStore);
             sslContextFactory.setKeyManagerPassword(serverKeyStore.getWebKeyManagerPassword());
          }
          else if (config == null)
          {
             SSLContext sslContext = sm.contextSm.getSecureSocketContext();
             sslContextFactory.setSslContext(sslContext);
          }
          else
          {
             if (serverKeyStore != null && serverKeyStore.isValid())
             {
                // Import sever SSL context
                ByteArrayInputStream stream = new ByteArrayInputStream(serverKeyStore.getKeyStore());
                KeyStore keyStore = KeyStore.getInstance("JKS");
                keyStore.load(stream, serverKeyStore.getKeyStorePassword().toCharArray());
                sslContextFactory.setKeyStore(keyStore);
                sslContextFactory.setKeyManagerPassword(serverKeyStore.getKeyManagerPassword());
             }
             else
             {
                SSLContext sslContext = sm.contextSm.getSecureSocketContext(config);
                sslContextFactory.setSslContext(sslContext);
             }
          }

          if (enableConscrypt && sslContextFactory != null)
          {
             if (Security.getProvider("Conscrypt") == null)
             {
                Security.addProvider(new OpenSSLProvider());
             }

             sslContextFactory.setProvider("Conscrypt");
          }

          return sslContextFactory;
       }

       /**
        * Create and configure a SSL server connector.
        *
        * @param   server
        *          The server instance.
        * @param   host
        *          The server host name or IP address.
        * @param   port
        *          The port number to listen.
        * @param   sslContextFactory
        *          The <code>SslContextFactory</code> instance.
        *
        * @return  The SSL server connector
        */
       private static ServerConnector configSslConnector(Server server,
                                                         String host,
                                                         int port,
                                                         SslContextFactory sslContextFactory)
       {
          return configSslConnector(server, host, port, sslContextFactory, new HttpConfiguration());
       }

       /**
        * Create and configure a SSL server connector.
        *
        * @param   server
        *          The server instance.
        * @param   host
        *          The server host name or IP address.
        * @param   port
        *          The port number to listen.
        * @param   sslContextFactory
        *          The <code>SslContextFactory</code> instance.
        * @param   httpConfig
        *          The common http configuration settings
        *
        * @return  The SSL server connector
        */
       private static ServerConnector configSslConnector(Server server,
                                                         String host,
                                                         int port,
                                                         SslContextFactory sslContextFactory,
                                                         HttpConfiguration httpConfig)
       {
          String protocol = HttpVersion.HTTP_1_1.asString();
          httpConfig.addCustomizer(new SecureRequestCustomizer());
          Scheduler scheduler = new ScheduledExecutorScheduler(
                String.format("Connector-Scheduler-%x", SCHEDULER_ID.incrementAndGet()), true);
          // for GWT development comment out SslConnectionFactory,
          // GTW's superdevmode doesn't run with SSL
          ServerConnector sslConnector = new ServerConnector(server,
             null, scheduler, null, -1, -1,
             new SslConnectionFactory(sslContextFactory, protocol),
             new HttpConnectionFactory(httpConfig));
          sslConnector.setHost(host);
          sslConnector.setPort(port);
          sslConnector.setReuseAddress(true);

          return sslConnector;
       }

       /**
        * Get host site local address.
        *
        * @return  Site local host address if any or <b>localhost</b> if none.
        */
       private String getHostLocalAddress()
       {
          String hostAddress = "localhost";
          try
          {
             InetAddress address = getSiteLocalAddress();
             if (address != null)
             {
                hostAddress = address.getHostAddress();
             }
          }
          catch (Exception e)
          {
             LOG.logp(Level.SEVERE,
                      "GenericWebServer.getHostLocalAddress()",
                      "",
                      "Error!",
                      e);
          }
          return hostAddress;
       }

       /**
        * Search on the network interfaces of this machine for
        * <code>InetAddress</code> which is a site local address.
        *
        * @return  The InetAddress of the first site local address.
        *
        * @throws  Exception
        *          If an error condition occur.
        */
       private InetAddress getSiteLocalAddress()
       throws Exception
       {
          // enumerate network interfaces
          Enumeration<NetworkInterface> netInterfaces =
             NetworkInterface.getNetworkInterfaces();
          if (netInterfaces != null)
          {
             while (netInterfaces.hasMoreElements())
             {
                NetworkInterface netInterface = netInterfaces.nextElement();
                if (netInterface.isUp())
                {
                   // enumerate addresses bound to this network interface.
                   Enumeration<InetAddress> addresses =
                      netInterface.getInetAddresses();
                   while (addresses.hasMoreElements())
                   {
                      InetAddress address = addresses.nextElement();
                      if (address.isSiteLocalAddress())
                      {
                         return address;
                      }
                   }
                }
             }
          }
          return null;
       }

       /**
        * Set the server context path root.
        *
        * @param   contextPath
        *          A string representing the context path root.
        */
       public void setContextPath(String contextPath)
       {
          this.contextPath = contextPath;
       }

       /**
        * Get the server context path root.
        *
        * @return  A string representing the context path root.
        */
       public String getContextPath()
       {
          return contextPath;
       }

       /**
        * Get server runtime port number.
        *
        * @return  The server runtime port number.
        */
       public int getPort()
       {
          return port;
       }

       /**
        * Get the server runtime host.
        *
        * @return  The server runtime host.
        *
        */
       public String getHost()
       {
          return (host == null) ? getHostLocalAddress() : host;
       }

       /**
        * Set imported key store
        *
        * @param   keyStore
        *          Imported key store if any or <code>null</code>
        */
       public void setKeyStore(ServerKeyStore keyStore)
       {
          this.serverKeyStore = keyStore;
       }

       /**
        * A session listener to clean the FWD context for destroyed sessions.
        */
       private static class SessionCleaner
       implements HttpSessionListener
       {
          /**
           * Notification when Jetty housekeeping will destroy inactive/timeout sessions.  This will terminate the
           * FWD session associated with this HTTP session.
           *
           * @param    se
           *           The session event.
           */
          @Override
          public void sessionDestroyed(HttpSessionEvent se)
          {
             HttpSession session = se.getSession();
             ContextSwitcher switcher = (ContextSwitcher) session.getAttribute("fwd.switcher");
             if (switcher != null)
             {
                Object sessionId = session.getId();
                SecurityManager sm = SecurityManager.getInstance();
                try
                {
                   sm.sessionSm.terminateSessionById(sessionId);
                }
                catch (RestrictedUseException e)
                {
                   throw new RuntimeException(e);
                }
             }
          }

          @Override
          public void sessionCreated(HttpSessionEvent se)
          {
             // no-op
          }
       }
    }