# WebServiceHandler.java

    /*
    ** Module   : WebServiceHandler.java
    ** Abstract : Handler for WebHandler calls.
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20190703 First version.
    ** 002 CA  20190710 Fixed a problem in addService, wrong WebHandler class was used.
    **     CA  20190723 The path matching is done case-insensitive.
    ** 003 CA  20200116 Javadoc fixes.
    **     GES 20200213 NPE protection in termination code.
    ** 004 CA  20200427 Fixed to allow session context details.
    ** 005 CA  20200514 Fixed error handling for handleWebService.
    ** 006 CA  20220405 Added authentication and authorization for web requests.  When this is enabled, the target
    **                  API call will be executed under the authenticated FWD context, and not the agent's context.
    ** 007 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 008 GBB 20230825 WebHandler renamed to WebDriverHandler.
    ** 009 CA  20240219 Target web handler resolution will use the explicit path match if it exists together with
    **                  a path token match like '/Test' vs '/{Other}'.
    ** 010 GBB 20240610 LegacyAppServerWork replaced by Consumer<LegacyServiceWorker>.
    ** 011 CA  20240925 When matching paths do determine the web handler, first configuration matching this
    **                  request's target will be resolved.
    **                  Ensure the configurations are sorted.
    ** 012 GBB 20250403 Replace service type with enum constant.
    ** 013 RB2 20250530 The handle method now calls the handle method with an extra parameter, the request. This
    **                  was done in order to handle multiple URLs in the Access-Control-Allow-Origin field
    */

    /*
    ** 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.util.*;

    import javax.servlet.http.*;

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

    import com.goldencode.p2j.util.*;

    /**
     * Specialized handler to process {@link com.goldencode.p2j.oo.web.WebHandler} paths.
     * <p>
     * <p>
     * Service registration is being performed via {@link SourceNameMapper}, which loads the
     * programs marked with <code>with-services</code>, via {@link #addService} API.
     * <p>
     * Any converted Java code mapped to a service must have a class-level {@link LegacyService}
     * annotation.  This annotation specifies one or more {@link LegacyWebPath paths} to be served by
     * this handler.
     * <p>
     * The implicit basepath is <code>/web</code>, and the implicit timeout is 10 seconds (set to 0
     * to disable it).  This timeout is read from the appserver's timeout.
     * <p>
     * The values which need to be configured in the directory are:
     * <ul>
     *    <li><code>WebHandler/basepath</code>, to specify the basepath.
     *        Defaults to <code>/web</code>.</li>
     *    <li><code>WebHandler/alias</code>, a process certificate alias associated with the workers
     *        which will process the WebHandler requests; these will delegate the call to the
     *        appserver.  This context is automatically marked as HEADLESS.</li>
     *    <li><code>WebHandler/keyStore</code>, the key store with the certificate to authenticate
     *        this process.  If is missing, the store will be built from the directory.</li>
     *    <li><code>WebHandler/storePassword</code>, the store password, only if
     *        <code>WebHandler/keyStore</code> is set.</li>
     *    <li><code>WebHandler/poolSize</code> - the pool size of workers.  Defaults to 10.</li>
     *    <li><code>WebHandler/appserver</code> - the name of the appserver handling the requests.
     *        This appserver must be in the same FWD server as the handler (can't use a remote one).
     *    </li>
     * </ul>
     * <p>
     * Each queue of workers is ordered by the number of pending tasks to be executed - for each
     * incoming request, the worker with the smallest number of pending tasks will be chosen.
     */
    public class WebServiceHandler
    extends LegacyServiceHandler
    {
       /** The singleton instance for this handler. */
       private static WebServiceHandler instance;

       /** The context handler. */
       private static ContextHandler handler;

       /** The exposed WebHandler services. */
       private Map<String, List<WebHandlerConfig>> services = new HashMap<>();

       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(WebServiceHandler.class);

       /**
        * Initialize this handler with the 'WebHandler' type and '/web' default basepath.
        */
       public WebServiceHandler()
       {
          super(ServiceType.WEB, "/web");
       }

       /**
        * Initialize the WebHandler worker(s) and start the workers.
        *
        * @return   The handler for WebHandler services.
        */
       public static AbstractHandler initialize()
       {
          synchronized (WebServiceHandler.class)
          {
             if (handler != null)
             {
                return handler;
             }

             instance = new WebServiceHandler();
             instance.initializeHandler();

             handler = new ContextHandler();
             handler.setContextPath(instance.getBasePath());
             handler.setHandler(instance);

             return handler;
          }
       }

       /**
        * Determine if this web service is configured to authenticate the APIs.
        *
        * @return   <code>true</code> if the authentication is enabled for this web service, in the directory.
        */
       public static boolean useWebServiceAuthentication()
       {
          synchronized (WebServiceHandler.class)
          {
             return instance != null && instance.useAuthorization();
          }
       }

       /**
        * Terminate the rest worker threads.
        */
       public static void terminate()
       {
          synchronized (WebServiceHandler.class)
          {
             if (instance != null)
             {
                instance.terminateWorkers();
             }
          }
       }

       /**
        * Register a legacy service associated with a {@link com.goldencode.p2j.oo.web.WebHandler}
        * implementation.
        *
        * @param    pls
        *           The annotation describing the service.
        * @param    cls
        *           The WebHandler implementation.
        */
       public static void addService(LegacyService pls, Class<?> cls)
       {
          synchronized (WebServiceHandler.class)
          {
             if (instance == null)
             {
                LOG.warning("Trying to register WebHandler service " + pls.name() +
                               ", but WebHandler service is not enabled.");
                return;
             }
          }

          if (!com.goldencode.p2j.oo.web.WebHandler.class.isAssignableFrom(cls))
          {
             throw new IllegalArgumentException(cls.toString() + " is not a sub-class of WebHandler!");
          }

          String section = pls.name().toLowerCase();
          List<WebHandlerConfig> cfgs = instance.services.get(section);
          if (cfgs == null)
          {
             instance.services.put(section, cfgs = new ArrayList<>());
          }
          for (LegacyWebPath path : pls.paths())
          {
             cfgs.add(new WebHandlerConfig(path.path(),
                                           path.order(),
                                           (Class<? extends WebDriverHandler>) cls));
          }

          // ensure they are sorted, 'initialize' is called to early, and no need for a 'postInitialize' as the
          // size of the configs is small
          Collections.sort(cfgs);
       }

       /**
        * Code to process web requests.
        */
       @Override
       public void handle(String              target,
                          Request             baseRequest,
                          HttpServletRequest  request,
                          HttpServletResponse response)
       throws IOException
       {
          if (baseRequest.isHandled())
          {
             return;
          }

          // first check the authentication
          super.handle(target, baseRequest, request, response);

          if (baseRequest.isHandled())
          {
             // authentication failed, nothing else to do
             return;
          }

          baseRequest.setHandled(true);

          String[] targetPaths = getPaths(target);
          WebHandlerConfig found = null;

          outer: for (String section : services.keySet())
          {
             List<WebHandlerConfig> cfgs = services.get(section);

             for (WebHandlerConfig cfg : cfgs)
             {
                if (cfg.matches(targetPaths))
                {
                   found = cfg;
                   break outer;
                }
             }
          }

          if (found == null)
          {
             response.setStatus(HttpStatus.NOT_FOUND_404);
             response.getOutputStream().write(("Can't find service for path " + target).getBytes());
             return;
          }

          WebHandlerConfig cfg = found;
          LegacyAppServerWork work = (worker) ->
          {
             // now do the authorization - must be in the worker thread
             String token;
             if (useAuthorization())
             {
                token = authorize(target, request, response);
                if (token == null)
                {
                   return;
                }
             }
             else
             {
                token = null;
             }

             try
             {
                AppServerInvocationResult result = worker.getHelper().handleWebService(instance.getTimeout(),
                                                                                       cfg.handler,
                                                                                       instance.getBasePath(),
                                                                                       cfg.paths,
                                                                                       token,
                                                                                       target,
                                                                                       request,
                                                                                       response);
                if (result.getError() != null)
                {
                   response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
                   try
                   {
                      response.getOutputStream().write(result.getError().getMessage().getBytes());
                   }
                   catch (IOException e)
                   {
                      // ignore
                   }
                }
             }
             finally
             {
                postHandle(token);
             }
          };

          handle(instance, work, target, response, request);
       }

       /**
        * A configuration for a WebHandler, as it was defined in <code>openedge.properties</code>.
        * This will sort them based on the original <code>handler#</code> key.
        */
       private static class WebHandlerConfig
       implements Comparable<WebHandlerConfig>
       {
          /** The split path served by this handler.*/
          private final String[] paths;

          /** The order. */
          private final int order;

          /** The handler. */
          private final Class<? extends WebDriverHandler> handler;

          /**
           * Configure this webhandler to service the given path.
           *
           * @param    path
           *           The served path.
           * @param    order
           *           The path order.
           * @param    handler
           *           The handler class.
           */
          public WebHandlerConfig(String path, int order, Class<? extends WebDriverHandler> handler)
          {
             this.paths = getPaths(path);
             this.order = order;
             this.handler = handler;
          }

          /**
           * Compare this to another instance, by their {@link #order}.
           */
          @Override
          public int compareTo(WebHandlerConfig o)
          {
             return this.order - o.order;
          }

          /**
           * Check if this configured web handler's paths match the given target.  The match doesn't take into
           * consideration if the configuration ends with a {@code /*} or not, this is always assumed - so, if
           * the configuration is a prefix for this target (considering path parameters), then this will be a
           * match.
           *
           * @param    target
           *           The target path to match.
           *
           * @return   See above.
           */
          public boolean matches(String[] target)
          {
             int plen = paths.length;

             if (plen == 0)
             {
                // here we match on anything
                return true;
             }

             String last = paths[plen - 1];
             if ("*".equals(last) || "**".equals(last))
             {
                plen = plen - 1;
             }

             if (plen == 0)
             {
                // match on anything
                return true;
             }

             if (plen > target.length)
             {
                // if more configured paths then this target, we can't match, as configuration can't be a prefix
                // for this configuration
                return false;
             }

             // target can have more parts as in this configuration, so walk only the configured parts
             for (int i = 0; i < plen; i++)
             {
                if (paths[i].startsWith("{"))
                {
                   // this is a path parameter, we can match
                   continue;
                }

                if (!paths[i].equalsIgnoreCase(target[i]))
                {
                   // this part doesn't match
                   return false;
                }

                // the current part matches, we can continue
             }

             // we reached here; it means all tokens in the configured path match, and if there is or not a '*'
             // suffix doesn't matter, we always match
             return true;
          }
       }
    }