EmbeddedWebHandler.java

/*
** Module   : EmbeddedWebHandler.java
** Abstract : Define handlers to run the embedded web app from within FWD.
**
** Copyright (c) 2018-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 CA  20180724 Created initial version
** 002 CA  20180815 adm_windows.json will be served from the embedded/adm_windows.json path.
**                  The DefaultHandler must not be registered here, otherwise the /gui path will
**                  not be found.
** 003 IAS 20210423 Make EmbeddedWebAppHandler.AuthWorker thread daemon.
**     CA  20210816 Do not set the request.handled to false (and do not handle the request if already handled).
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 GBB 20230825 Complete rework of the class. Renamed from EmbeddedWebAppHandler to EmbeddedWebHandler.
**                  Remote spawn implementation removed.
** 006 GBB 20231027 Template placeholder constant for auto-login client url renamed to AUTO_LOGIN_DATA.
** 007 GBB 20240517 Resolving web pages template placeholder PLACEHOLDER_PROXY_PATH.
** 008 GBB 20240530 Configurable /embedded path.
** 009 TJD 20240415 Allow serving JS libraries from webjars
** 010 GBB 20240709 DriverType moved to outer class WebDriverType.
*/
/*
** 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 com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.web.*;

import javax.servlet.http.*;

import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.util.resource.*;

import java.lang.*;
import java.util.*;

/**
 * Define handlers to run the embedded web app from within fwd.  The handlers are run on the
 * <code>/embedded</code>, where <code>/embedded/fwd</code> will serve content from the 
 * <code>com/goldencode/p2j/ui/client/gui/driver/web/res/</code> package and the
 * <code>/embedded/custom</code> will server content from the <code>embedded/</code> sub-package
 * of the application's package root (as retrieved via {@link SourceNameMapper#getPackageRoot}. 
 */
public class EmbeddedWebHandler
extends WebDriverHandler
{
   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(EmbeddedWebHandler.class);
   
   /** The path to for the embedded configs. */
   private static final String TARGET_EMBEDDED_CONFIGS = TARGET_EMBEDDED + "/adm_windows.json";

   /** The path for the embedded custom resources. */
   private static final String TARGET_EMBEDDED_CUSTOM = TARGET_EMBEDDED + "/custom";

   /** The path for the embedded base web resources. */
   private static final String TARGET_EMBEDDED_RES = TARGET_EMBEDDED + "/fwd";

   /** The path for the embedded base web resources. */
   private static final String EMBEDDED_RES_PATH = "com/goldencode/p2j/ui/client/gui/driver/web/res/";

   /** The filename of the resource for the login page partial. */
   private static final String HTML_PARTIAL_HTML_FILENAME = "web_partial_login_embedded.html";

   /** Template placeholder for the embedded context path. See {@link #TARGET_EMBEDDED}. */
   protected static final String PLACEHOLDER_EMBEDDED_PATH = "${embeddedPath}";
   
   /**
    * Template placeholder for the context path before /webres. Can be the proxy path or empty (root). 
    * Different from {@link #PLACEHOLDER_PROXY_PATH} that can be omitted with
    * {@link #REMOVE_PROXY_PATH_IN_WEB_PAGES}.
    */
   protected static final String PLACEHOLDER_WEBRES_PATH = "${webresPath}";

   /**
    * Private constructor for EmbeddedWebHandle. Initializes all the handlers for the embedded web app. 
    * Reads directory configs from the server context, because handle() runs without security context.
    */
   EmbeddedWebHandler()
   {
      super("client:cmd-line-option:startup-procedure=emain.p", "client:web:embedded=true");

      addHandler(new HandlerCollection(
         createContextHandler(TARGET_EMBEDDED_RES, EMBEDDED_RES_PATH),
         createContextHandler(TARGET_EMBEDDED_CUSTOM, PKG_ROOT + "embedded/"),
         // in build.gradle, add a WARNING for each zip artifact which is hard-coded here.
         // do not expose the "/" root resource folder - as this will allow access to any class path
         // file, which includes jar'ed classes, configurations, etc
         // this needs to be inline with build.gradle versions
         new DojoToolkitHandler(TARGET_EMBEDDED),
         new WebResourceHandler(TARGET_EMBEDDED, "/bootstrap", "META-INF/resources/webjars/bootstrap/3.4.1"),
         new WebResourceHandler(TARGET_EMBEDDED, "/d3", "META-INF/resources/webjars/d3/4.13.0/build"),
         new WebResourceHandler(TARGET_EMBEDDED, "/fonts", "fonts"),
         new WebResourceHandler(TARGET_EMBEDDED, "/jquery", "META-INF/resources/webjars/jquery/3.7.1"),
         new WebResourceHandler(TARGET_EMBEDDED, "/jquery-ui", "META-INF/resources/webjars/jquery-ui/1.13.2"),
         new WebResourceHandler(TARGET_EMBEDDED, "/material-components-web", "material-components-web"),
         new WebResourceHandler(TARGET_EMBEDDED, "/material-design-icons", "material-design-icons"),
         new WebResourceHandler(TARGET_EMBEDDED, "/tabulator", "tabulator-master-2.12.0")
      ));
   }

   /**
    * Creates a new context handler serving class path resources from the given path.
    *
    * @param    context
    *           The context used to server the resources.
    * @param    resPath
    *           The path were the resources reside.
    *
    * @return   A new handler.
    */
   private static ContextHandler createContextHandler(String context, String resPath)
   {
      ResourceHandler res = new ResourceHandler();
      res.setDirectoriesListed(false);
      ContextHandler ctx = new ContextHandler(context);
      ctx.setBaseResource(Resource.newClassPathResource(resPath));
      ctx.setHandler(res);
      
      return ctx;
   }

   /**
    * Processes embedded web requests.
    *
    * @param    target
    *           The target path.
    * @param    base
    *           The HTTP request, Jetty style.
    * @param    request
    *           The HTTP request.
    * @param    response
    *           The HTTP response.
    */
   @Override
   public void handle(String target, Request base, HttpServletRequest request, HttpServletResponse response)
   {
      try
      {
         super.handle(target, base, request, response);

         if (base.isHandled())
         {
            return;
         }

         if (!target.equals(TARGET_EMBEDDED) &&
            !target.equals(TARGET_EMBEDDED_CONFIGS))
         {
            // do not set 'handled' to false, as it may have to be processed by another handler.
            return;
         }

         base.setHandled(true);

         String method = request.getMethod().toUpperCase();
         if (target.equals(TARGET_EMBEDDED) && method.equals("POST"))
         {
            handleStartClientRequest(base, request, response, true, WebDriverType.EMBEDDED);
            return;
         }

         if (!method.equals("GET"))
         {
            response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
            return;
         }

         String clientUrl = null;
         // TODO: enable auto login (and logout page) when cross document msgs can handle it
         /*if (target.equals(TARGET_EMBEDDED))
         {
            SpawnerResult spawnerResult = handleSsoAutoLogin(base, request, true, DriverType.EMBEDDED);
            if (spawnerResult != null)
            {
               // on successful auto login, the new client url will be embedded in the login page
               clientUrl = spawnerResult.uri;
            }
         }*/

         handleStatic(target, request, response, clientUrl);
      }
      catch (Throwable t)
      {
         LOG.warning("Unexpected exception:", t);
         sendError(response, SupportedHttpCode.UNEXPECTED_ERROR, null);
      }
   }
   
   /**
    * Handles HTTP requests for static resources, i.e. index / SSO login page and configs.
    *
    * @param    target
    *           The target path.
    * @param    request
    *           The HTTP request.
    * @param    response
    *           The HTTP response.
    * @param    clientUrl
    *           The spawned client URL.
    */
   private void handleStatic(String target,
                             HttpServletRequest request,
                             HttpServletResponse response,
                             String clientUrl)
   {
      if (target.equals(TARGET_EMBEDDED_CONFIGS))
      {
         serveStatic(response,
                     "application/json",
                     null,
                     PKG_ROOT + "embedded/adm_windows.json");
      }
      else if (target.equals(TARGET_EMBEDDED))
      {
         String serverProxyPath = getServerProxyPath(resolveForwardedHost(request));
         String htmlContent = loadPartial(HTML_PARTIAL_HTML_FILENAME,
                                          PKG_RES_DIR,
                                          new HashMap<String, String>() {{
                                             put(PLACEHOLDER_WEBRES_PATH, serverProxyPath);
                                             put(PLACEHOLDER_PROXY_PATH,
                                                 REMOVE_PROXY_PATH_IN_WEB_PAGES ? "" : serverProxyPath);
                                             put(PLACEHOLDER_EMBEDDED_PATH, TARGET_EMBEDDED);
                                          }});
         
         SsoAuthenticator ssoAuthenticator = SecurityManager.getInstance().ssoSm.getSsoAuthenticator();
         if (ssoAuthenticator != null)
         {
            String ssoInflatedTemplate = ssoAuthenticator.getLoginPage(htmlContent);
            if (ssoInflatedTemplate != null)
            {
               htmlContent = ssoInflatedTemplate;
            }
         }
         
         if (htmlContent == null)
         {
            LOG.fine("File not found " + HTML_PARTIAL_HTML_FILENAME);
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
         }
         
         Map<String, String> placeholderValuePairs = new HashMap<>();
         if (clientUrl != null)
         {
            placeholderValuePairs.put(AUTO_LOGIN_DATA, clientUrl);
         }
         placeholderValuePairs.put(PLACEHOLDER_HTML_TEMPLATE, htmlContent);
         placeholderValuePairs.put(PLACEHOLDER_TITLE, PAGE_TITLE_LOGIN == null ?
            "Login (Embedded Gui Web Client)" :
            PAGE_TITLE_LOGIN);
         placeholderValuePairs.put(PLACEHOLDER_PROXY_PATH, getServerProxyPath(request));
         
         serveHtml(response, placeholderValuePairs, getResPathCurrentDir("web_landing.html"));
      }
      else
      {
         response.setStatus(HttpServletResponse.SC_NOT_FOUND);
      }
   }
}