WebServer.java

/*
** Module   : WebServer.java
** Abstract : provides an embedded web server based on Jetty
**
** Copyright (c) 2009-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description----------------------
** 001 JJC 20090107   @41129 Created the first version of the file.
** 002 JJC 20090114   @41130 Add initialize() so WebServer can be started
**                           by the StandardServer.loadStartup().
** 003 NVS 20090304   @41399 Reworked and brought to compliance with the
**                           P2J coding standards. Renamed.
** 004 NVS 20090326   @41692 Added support for request parameters. They
**                           are coded using "?p1&p2..." syntax in URL.
**                           The "debug" parameter, if specified, causes
**                           the applet tag to specify JVM parameters for
**                           creating a debug mode applet's JVM.
** 005 SIY 20090513   @42158 Use daemon threads instead of regular ones to 
**                           prevent server hang after graceful shutdown.
** 006 NVS 20090601   @42512 A custom account extension client side plugin is 
**                           added to the ARCHIVE keyword of the applet page,
**                           if the P2J system is configured with one.
** 007 NVS 20090602   @42544 Fixed #006 to provide the archive file name, not
**                           the class name.
** 008 CA  20090603   @42554 Fixed #007: a NPE error when no plugin is provided.
** 009 NVS 20090615   @42695 Protected jetty server from serving arbitrary
**                           files. Now only the admin base and extension jars
**                           will be served through jetty.
** 010 NVS 20090617   @42722 Fixed a NPE when there is no admin extensions.
**                           The admin server won't start if the main p2jadmin
**                           jar is not found.
** 011 NVS 20090617   @42725 Fixed the client main jar file search procedure
**                           to avoid false positives for the p2j.jar.
** 012 NVS 20090821   @43712 Admin port is now searched correctly scoped to the
**                           per server scope.
** 013 GES 20100421   @44830 Handle host address/name better by using the
**                           base_request instead of request for getting the
**                           "local name".
** 014 GES 20110929          Moved this code here from AdminGate. Refactored it
**                           to be more generic.
** 015 CS  20130213          Added conversion support for Server, disambiguated 
**                           the Server class.
** 016 MAG 20131101          Upgrade to jetty 9.1 embedded server.  
** 017 MAG 20131108          Add WebSocket's capabilities by extending the 
*                            new GenericWebServer.  
** 018 MAG 20140217          Add terminate method.          
** 019 SBI 20171116          Changed to set ServerKeyStore.
** 020 CA  20200514          Added getURL(), to return the URL and path served by this server.
** 021 SBI 20210413          Added getWebContentHandlers() to public static content.
**         20210621          Updated usages of Utils.getDirectoryNodeBeanList
**         20210812          Added createContextHandler() method to encapsulate and reuse this logic.
**         20211003          Moved common constants to HttpConfigurationConstants.
** 022 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 023 SBI 20231208          Fixed the default idle timeout for web server http configuration and replaced coded
**                           parameters with corresponding defined constants.
** 024 GBB 20240425          searchResourceJars() moved to JarUtil.
** 025 GBB 20240709          Moving here constants from HttpConfigurationConstants, no longer used anywhere else.
*/
/*
** 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.main;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;

import com.goldencode.p2j.util.logging.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.util.resource.*;

import com.goldencode.p2j.classloader.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.web.*;

/**
 * Provides an embedded web server based on Jetty 9.1
 * <p>
 * In particular, Jetty will be started using keystore info from p2j config
 * that is already in memory.
 */
public class WebServer
extends GenericWebServer
{
   /** HTTPS default port for listening. */
   public static final int HTTPS_PORT = 9443;
   
   /** Anonymous log instance. */
   private static final CentralLogger LOG = CentralLogger.get(WebServer.class.getName());
   
   /** The "maxOutputBufferSize" parameter name */
   private static final String MAX_OUTPUT_BUFFER_SIZE_PARAM = "maxOutputBufferSize";

   /** The "maxOutputAggregationSize" parameter name */
   private static final String MAX_OUTPUT_AGGREGATION_SIZE_PARAM = "maxOutputAggregationSize";

   /** The "webServerMaxIdleTimeout" parameter name */
   private static final String WEB_SERVER_MAX_IDLE_TIMEOUT_PARAM = "webServerMaxIdleTimeout";

   /** The "maxResponseHeaderSize" parameter name */
   private static final String MAX_RESPONSE_HEADER_SIZE_PARAM = "maxResponseHeaderSize";

   /** The "maxRequestHeaderSize" parameter name */
   private static final String MAX_REQUEST_HEADER_SIZE_PARAM = "maxRequestHeaderSize";
   
 
   /** Singleton instance of this class. */
   private static WebServer instance = null;
   
   /** Server port in use. */
   private int port = HTTPS_PORT;
   
   /**
    * Constructor.
    *
    * @param   handlers
    *          List of handlers to register.
    *           
    * @throws  Exception
    *          If something goes wrong.
    */
   private WebServer(org.eclipse.jetty.server.Handler[] handlers)
   throws Exception
   {
      // server-scoped directory search for the admin port number
      port = Utils.getDirectoryNodeInt(null, "adminPort", HTTPS_PORT, false);
      
      int maxOutputBufferSize = Utils.getDirectoryNodeInt(null,
                                                          MAX_OUTPUT_BUFFER_SIZE_PARAM,
                                                          OUTPUT_BUFFER_SIZE,
                                                          false);
      getHttpConfiguration().setOutputBufferSize(maxOutputBufferSize);
      int maxOutputAggregationSize = Utils.getDirectoryNodeInt(null,
                                                               MAX_OUTPUT_AGGREGATION_SIZE_PARAM,
                                                               OUTPUT_AGGREGATION_SIZE,
                                                               false);
      if (maxOutputAggregationSize != OUTPUT_AGGREGATION_SIZE)
      {
         getHttpConfiguration().setOutputAggregationSize(maxOutputAggregationSize);
      }
      int maxIdleTimeout = Utils.getDirectoryNodeInt(null,
                                                     WEB_SERVER_MAX_IDLE_TIMEOUT_PARAM,
                                                     CONNECTION_TIMEOUT,
                                                     false);
      getHttpConfiguration().setIdleTimeout(maxIdleTimeout);
      
      int maxResponseHeaderSize = Utils.getDirectoryNodeInt(null,
                                                            MAX_RESPONSE_HEADER_SIZE_PARAM,
                                                            RESPONSE_HEADER_SIZE,
                                                            false);
      if (maxResponseHeaderSize != RESPONSE_HEADER_SIZE)
      {
         getHttpConfiguration().setResponseHeaderSize(maxResponseHeaderSize);
      }
      int maxRequestHeaderSize = Utils.getDirectoryNodeInt(null,
                                                           MAX_REQUEST_HEADER_SIZE_PARAM,
                                                           REQUEST_HEADER_SIZE,
                                                           false);
      if (maxRequestHeaderSize != REQUEST_HEADER_SIZE)
      {
         getHttpConfiguration().setRequestHeaderSize(maxRequestHeaderSize);
      }
      
      setKeyStore(ServerKeyStore.getStore());
      
      addHandler(handlers);
      startup(null, port);
   }
   
   /**
    * Get the URL on which this server is running.
    * 
    * @return   See above.
    */
   public static String getURL()
   {
      synchronized (WebServer.class)
      {
         if (instance == null)
         {
            throw new NullPointerException("The FWD web server was not started!");
         }
         
         return String.format("https://%s:%d%s", 
                              instance.getHost(),
                              instance.getPort(),
                              instance.getContextPath());
      }
   }
   
   /**
    * Builds the list of web handlers for managing static web content given by the web-static-content
    * directory settings.
    * 
    * @return   The list of web handlers for managing static web content
    * 
    * @throws   Throwable
    *           Iff errors occur while reading web-static-content settings from the directory
    */
   public static ContextHandler[] getWebContentHandlers()
   throws Throwable
   {
      DirectoryService ds = DirectoryService.getInstance();
      
      List<WebContext> list = Utils.<WebContext>getDirectoryNodeBeanList(
                                                           ds,
                                                           "web-static-content",
                                                           null,
                                                           WebContext.class,
                                                           false,
                                                           false);
      
      List<ContextHandler> webHandlers = new ArrayList<>(list.size());
      
      for (WebContext entry : list)
      {
         try
         {
            ContextHandler webHandler = entry.createContextHandler(StandardServer::getResourceJarPaths);
            webHandlers.add(webHandler);
         }
         catch (IOException e)
         {
            LOG.log(Level.SEVERE, e.getMessage());
         }
         catch (IllegalArgumentException e1)
         {
            LOG.log(Level.WARNING, e1.getMessage());
         }
      }
      
      return webHandlers.toArray(new ContextHandler[webHandlers.size()]);
   }

   /**
    * Called by StandardServer.loadStartup() to start the Web Server.
    *
    * @param   handlers
    *          List of handlers to register.
    */
   public static void initialize(org.eclipse.jetty.server.Handler[] handlers)
   {
      synchronized (WebServer.class)
      {    
         if (instance == null)
         {
            // instantiate this class
            try
            {
               instance = new WebServer(handlers);
            }
            catch (Exception exc)
            {
               LOG.logp(Level.SEVERE,
                        "WebServer.initialize()",
                        "",
                        "Could not startup web server!",
                        exc);
            }
         }
      }
   }

   /**
    * Shutdown web server instance if any.
    */
   public static void terminate()
   {
      synchronized (WebServer.class)
      {    
         if (instance != null)
         {
            try
            {
               instance.shutdown();
               instance.join();
            }            
            catch (Exception exc)
            {
               LOG.logp(Level.SEVERE,
                        "WebServer.terminate()",
                        "",
                        "Could not shutdown web server!",
                        exc);
            }
         }
      }
   }
   
   /**
    * Holds the web content resource description.
    */
   public static class WebContext
   {
      /**
       * The public context path
       */
      public String context;
      
      /**
       * The path to the target resource
       */
      public String resource;
      
      /**
       * Welcome file delivered by the server to the client
       */
      public String file;
      
      /**
       * Creates the context handler based on this web context.
       * 
       * @return   The context handler.
       * 
       * @throws   IOException
       *           If an IO error occurs when trying to create new resource by the given path
       * @throws   IllegalArgumentException
       *           If the context path isn't set and/or the resource isn't found 
       */
      public ContextHandler createContextHandler(Supplier<List<String>> propathSupplier)
      throws IOException
      {
         if (context == null)
         {
            throw new IllegalArgumentException("The context path isn't set");
         }
         
         ResourceHandler resourceHandler = new ResourceHandler();
         
         if (file != null && !file.trim().isEmpty())
         {
            resourceHandler.setWelcomeFiles(new String[] {file.trim()});
         }
         resourceHandler.setDirectoriesListed(false);
         ContextHandler webHandler = new ContextHandler();
         webHandler.setContextPath(context);
         webHandler.setHandler(resourceHandler);
         
         Function<JarClassLoader, Function<String, String>> searchFn = jcl -> (n -> jcl.getResourceName(n, true));
         JarUtil.JarResource jarEntry = JarUtil.searchResourceJars(LOG,
                                                                   resource,
                                                                   searchFn,
                                                                   null,
                                                                   propathSupplier);
         URL urlJarResource = null;
         if (jarEntry != null)
         {
            urlJarResource = jarEntry.getLoader().getResource(jarEntry.getName());
         }
         Resource targetResource = null;
         
         if (urlJarResource != null)
         {
            targetResource = Resource.newResource(urlJarResource);
         }
         else
         {
            targetResource = Resource.newResource(resource);
         }
         
         if (targetResource == null || !targetResource.exists())
         {
            throw new IllegalArgumentException("The resource isn't found by the given path [" + resource +"]");
         }
         
         webHandler.setBaseResource(targetResource);
         
         return webHandler;
      }
   }
}