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