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