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
      }
   }
}