# VirtualDesktopWebHandler.java

    /*
    ** Module   : VirtualDesktopWebHandler.java
    ** Abstract : Web handler for the Virtual Desktop. Spawns a client proces with embedded web server and
    **            redirects to the url.
    **
    ** Copyright (c) 2023-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description---------------------------------
    ** 001 GBB 20230712 Created initial version
    ** 002 GBB 20231027 Adding storageId to the html template tag for auto-login.
    ** 003 GBB 20231113 Handling session forking.
    ** 004 GBB 20231127 Field PLACEHOLDER_IS_AUTO_LOGIN made private.
    ** 005 GBB 20240214 Resolving deviceId and adding it to the spawn args to be available in the session
    **                  post-init.
    ** 006 GBB 20240517 Resolving web pages template placeholder PLACEHOLDER_PROXY_PATH.
    **                  Replacing isGui with DriverType.
    ** 007 GBB 20240530 loadPartial() reworked to allow custom placeholders.
    ** 008 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 java.io.*;
    import java.lang.*;
    import java.util.*;

    import javax.servlet.http.*;

    import com.fasterxml.jackson.databind.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.logging.*;
    import org.eclipse.jetty.server.*;

    /**
     * Starts a web client and redirects the requestor to the newly spawned client process / web server URI.
     * <p>
     * On GET method a web page containing an authentication form is returned to caller.
     * The user should enter the user name and the password used to authenticate on
     * the target client platform (Linux / Windows) and press the "Login" button to submit the
     * form (which will submit/POST the form).
     * <p>
     * On POST method using the user submitted credentials a new web client process is spawned
     * with the user's operating-system login environment. On web client URI notification the user's
     * browser is redirected to this URI. If no response from the spawned client occurs within a
     * specified amount of time, the authentication page is reloaded with an error message.
     */
    public class VirtualDesktopWebHandler
    extends WebDriverHandler
    {

       /** Template placeholder for boolean flag indicating if auto-login is enabled. */
       private static final String PLACEHOLDER_IS_AUTO_LOGIN = "${isAutoLoginEnabled}";

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

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

       /**
        * Private constructor for VirtualDesktopWebHandler. Reads directory configs from the server context,
        * because handle() runs without security context.
        */
       VirtualDesktopWebHandler()
       {
          super(null, "client:web:embedded=false");
       }

       /**
        * Handle Virtual Desktop HTTP requests.
        *
        * @param    target
        *           The target of the request - either a URI or a name.
        * @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;
             }

             boolean chui = TARGET_CHUI_ROOT.equalsIgnoreCase(target);
             boolean gui = TARGET_GUI_ROOT.equalsIgnoreCase(target);
             boolean guiLogout = TARGET_GUI_LOGOUT.equalsIgnoreCase(target);
             boolean chuiLogout = TARGET_CHUI_LOGOUT.equalsIgnoreCase(target);

             // only process for the known contexts
             if (!chui && !gui && !guiLogout && !chuiLogout)
             {
                return;
             }
             WebDriverType driverType = gui || guiLogout ? WebDriverType.GUI : WebDriverType.CHUI;

             base.setHandled(true);

             String method = request.getMethod().toUpperCase();
             if (guiLogout || chuiLogout)
             {
                if (!isSsoEnabled())
                {
                   try
                   {
                      response.sendError(HttpServletResponse.SC_NOT_FOUND);
                   }
                   catch (IOException e)
                   {
                      response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                   }
                   return;
                }

                if (method.equals("GET"))
                {
                   serveLogoutPage(request, response, driverType);
                }
                else if (method.equals("POST"))
                {
                   handleLogoutRequest(request, response, driverType);
                }
                else
                {
                   response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                }
                return;
             }

             switch (method)
             {
                case "GET":
                   serveLandingPage(base, request, response, driverType);
                   break;
                case "POST":
                   handleStartClientRequest(base,
                                            request,
                                            response,
                                            isSsoEnabled(),
                                            driverType);
                   break;
                default:
                   response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
                   break;
             }
          }
          catch (Throwable t)
          {
             LOG.warning("Unexpected exception:", t);
             sendError(response, SupportedHttpCode.UNEXPECTED_ERROR, null);
          }
       }

       /**
        * Handle POST /logout requests.
        *
        * @param    request
        *           The object of the request.
        * @param    response
        *           The object of the response.
        * @param    driverType
        *           The client type as GUI / ChUI.
        */
       private void handleLogoutRequest(HttpServletRequest request,
                                        HttpServletResponse response,
                                        WebDriverType driverType)
       {
          SsoAuthenticator ssoAuthenticator = SecurityManager.getInstance().ssoSm.getSsoAuthenticator();
          Cookie invalidatedCookie = ssoAuthenticator.logout(request.getCookies());
          if (invalidatedCookie == null)
          {
             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
          }
          else
          {
             response.addCookie(invalidatedCookie);
             try
             {
                response.sendRedirect(driverType.getLoginTarget());
             }
             catch (IOException e)
             {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
             }
          }
       }

       /**
        * Delivers the logout page. Available only with SSO enabled.
        *
        * @param    request
        *           The HTTP request.
        * @param    response
        *           The HTTP response.
        * @param    driverType
        *           The client type as GUI / ChUI.
        */
       private void serveLogoutPage(HttpServletRequest request, HttpServletResponse response, WebDriverType driverType)
       {
          String logoutPageTitle = PAGE_TITLE_LOGOUT == null ?
             "Logout (" + driverType.toString() + " Web Client)" :
             PAGE_TITLE_LOGOUT;

          boolean isAutoLoginEnabled = SecurityManager.getInstance()
             .ssoSm
             .getSsoAuthenticator()
             .getAuthCookieName() != null;

          Map<String, String> placeholders = new HashMap<>();
          placeholders.put(PLACEHOLDER_TITLE, logoutPageTitle);
          placeholders.put(PLACEHOLDER_IS_AUTO_LOGIN, String.valueOf(isAutoLoginEnabled));
          placeholders.put(PLACEHOLDER_PROXY_PATH, getServerProxyPath(request));

          serveHtml(response,
                    placeholders,
                    PKG_ROOT + PKG_RES_DIR + "web_logout.html",
                    getResPathCurrentDir("web_logout.html"));
       }

       /**
        * Handles GET requests. Returns the login landing page. With SSO auto-login and session fork certain
        * templates are inflated to allow front-end redirect to the newly spawned web client.
        *
        * @param    base
        *           The base request.
        * @param    request
        *           The object of the request.
        * @param    response
        *           The object of the response.
        * @param    driverType
        *           The client type as GUI / ChUI.
        */
       private void serveLandingPage(Request base,
                                     HttpServletRequest request,
                                     HttpServletResponse response,
                                     WebDriverType driverType)
       {
          WebDriverRequestParameters requestParameters = getRequestParameters(base, request);

          String deviceId = setDeviceId(requestParameters, response);

          SecurityManager sm = SecurityManager.getInstance();
          SsoAuthenticator ssoAuthenticator = sm.ssoSm.getSsoAuthenticator();
          boolean isSso = ssoAuthenticator != null;

          Map<String, String> placeholderValuePairs = new HashMap<>();
          placeholderValuePairs.put(PLACEHOLDER_TITLE,
                                    PAGE_TITLE_LOGIN != null ? PAGE_TITLE_LOGIN :
                                       ssoAuthenticator == null ?
                                          "Operating System Login (" + driverType.toString() + " Web Client)" :
                                          "Login (" + driverType.toString() + " Web Client)");
          placeholderValuePairs.put(PLACEHOLDER_PROXY_PATH, getServerProxyPath(request));

          boolean isSessionForked = handleSessionFork(requestParameters,
                                                      sm,
                                                      driverType,
                                                      isSso,
                                                      deviceId,
                                                      placeholderValuePairs);

          boolean isSuccessfulAutoLogin = false;
          if (!isSessionForked && isSso)
          {
             SpawnerResult spawnerResult = handleSsoAutoLogin(requestParameters, deviceId, driverType);

             String clientUrl = spawnerResult != null ? spawnerResult.uri : null;
             if (clientUrl != null)
             {
                isSuccessfulAutoLogin = true;
                // the new client url will be injected in the landing page as template content
                placeholderValuePairs.put(AUTO_LOGIN_DATA,
                                          new ObjectMapper().createObjectNode()
                                                            .put("url", clientUrl)
                                                            .put("storageId", spawnerResult.storageId)
                                                            .toString());
             }
          }

          if (!isSessionForked && !isSuccessfulAutoLogin)
          {
             // normal login page load
             String loginPartial = loadLoginPartial(response, ssoAuthenticator, requestParameters.getForwardedHost());
             if (loginPartial == null)
             {
                return;
             }
             placeholderValuePairs.put(PLACEHOLDER_HTML_TEMPLATE, loginPartial);
          }

          serveHtml(response, placeholderValuePairs, getResPathCurrentDir("web_landing.html"));
       }

       /**
        * Tries to load the login form content from the custom root package and then the current FWD package
        * and returns the content of the file as string.
        *
        * @param    response
        *           The HTTP response.
        * @param    ssoAuthenticator
        *           The SSO Authenticator.
        * @param    forwardedHost
        *           The proxy host.
        *
        * @return   The login partial template as string.
        */
       private String loadLoginPartial(HttpServletResponse response,
                                       SsoAuthenticator ssoAuthenticator,
                                       String forwardedHost)
       {
          String htmlContent = loadPartial(HTML_PARTIAL_HTML_FILENAME,
                                           PKG_RES_DIR,
                                           new HashMap<String, String>() {{
                                              put(PLACEHOLDER_PROXY_PATH, getServerProxyPath(forwardedHost));
                                           }});

          if (ssoAuthenticator != null)
          {
             String ssoInflatedTemplate = ssoAuthenticator.getLoginPage(htmlContent);
             if (ssoInflatedTemplate != null)
             {
                htmlContent = ssoInflatedTemplate;
             }
          }

          if (htmlContent == null)
          {
             LOG.warning("File not found " + HTML_PARTIAL_HTML_FILENAME);
             response.setStatus(HttpServletResponse.SC_NOT_FOUND);
             return null;
          }

          return htmlContent;
       }
    }