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