WebDriverHandler.java
/*
** Module : WebDriverHandler.java
** Abstract : Base class for Virtual Desktop and Embedded web handlers (web gui/chui launch).
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 MAG 20131113 Created initial version
** 002 MAG 20131129 Add authentication page. Handle GET and POST requests.
** 003 MAG 20140206 Send URL as referrer address via configuration file.
** 004 CA 20140206 Changes related to refactoring the spawner infrastructure (allow P2J process
** clients to be started, beside web chui clients).
** 005 MAG 20140303 Add authorization token.
** 006 MAG 20140723 Send authorization token as a query parameter instead a cookie.
** 007 OM 20150108 Passed null map as additional environment to WebClientSpawner.spawn().
** 008 GES 20150127 Cleanup code formatting.
** 009 GES 20150311 Modified this to support both chui and gui web clients.
** 010 SBI 20160205 Changed the redirect HTTP response to the "text/html" response containing
** the redirect path in its body and changed the error page response to
** the error message to the ajax client.
** GES 20160121 Added static implementation of WebClientLauncher interface for remote web
** client spawning. Reworked spawning implementation for embedded mode.
** Eliminated use of PlatformHelper, since that was testing the server OS and
** had no relation to the platform on which spawning was occurring.
** 011 CA 20160428 Added command-line configuration options for web-clients.
** 012 SBI 20170628 Changed to manage web clients.
** 013 SBI 20171023 Added getClientAddress and CLIENT_IP client address parameter key.
** 014 SBI 20181016 Added UI theme setup in the virtual desktop mode and set login form styles.
** 015 SBI 20210514 Added OPTIONS handler.
** 016 SBI 20230306 Added new exception that all network resource are used up, followed new implemented logic
** to spawn client process only after the network port is allocated successfully.
** 017 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 018 SBI 20230615 Fixed incorrect codes that hide the spawner error codes.
** 019 GBB 20230825 Complete rework. Renamed from WebHandler to WebDriverHandler. Now a base class for both
** virtual desktop and embedded concrete implementations.
** 020 GBB 20231027 Adding storageId to the data injected in the html template element.
** 021 GBB 20231110 Fix for empty password leading to a missing argument in passwordless spawn.
** 022 GBB 20231113 Adding handleSessionFork.
** Cleanup for web:referrer:url.
** Adding session description from SsoAuthenticator to CLIENT_UUID_SESS_DESCR_PAIRS.
** 023 GBB 20231127 Changes to the theme options: an empty option added, no default selected.
** 024 GBB 20240111 Moves isValidTheme() to LogicalTerminal.isValidThemeClass().
** Default theme option to be selected.
** 025 GBB 20240214 Resolving deviceId and adding it to the spawn args to be available in the session
** post-init. One browser policy implemented.
** 026 GBB 20240229 Adding log for attempt to start forked session with invalid token.
** 027 HC 20240513 Moved SPREADSHEET backend (Keikai web application and the related FWD
** integration) from client to server.
** 028 GBB 20240515 Adding new method filterOutValidLoginParams() and resolving webClient configs for
** login params. Passing in the login url query params to the logout url.
** 029 GBB 20240517 Adding proxy context path to web page urls.
** 030 GBB 20240530 loadPartial() reworked to allow custom placeholders.
** REMOVE_PROXY_PATH_IN_WEB_PAGES made protected.
** 031 GBB 20240709 Hard-coded config names replaced by ConfigItem constants. Store web client builder
** options for each web context in driverOptionsMap and reset the overrides with each new
** spawn request. Split the runtime web options in multiple fields for proper merge after
** the project token is resolved.
** 032 GBB 20240730 Resolve the OS user once for all requests.
** 033 CA 20250221 If the SSO plugin specifies OS credentials (username/password), use those.
*/
/*
** 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.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.translation.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
import org.eclipse.jetty.http.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.util.*;
import javax.servlet.http.*;
import java.io.*;
import java.lang.ref.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
import java.util.Scanner;
import java.util.concurrent.*;
import java.util.logging.*;
import java.util.regex.*;
/** Base class for Virtual Desktop and Embedded web handlers. */
public abstract class WebDriverHandler
extends HandlerCollection
{
/** Map of client uuid : fwd user for sso. */
public static final ConcurrentMap<String, String> CLIENT_UUID_FWD_USERS = new ConcurrentHashMap<>();
/** Flag to indicate if the One Browser Policy is enabled. */
public static final boolean ONE_BROWSER_POLICY = ConfigItem.ONE_BROWSER_POLICY.read(false);
/** The logout page context path. */
private static final String CONTEXT_PATH_LOGOUT = "/logout";
/** The root portion of the GUI client request name (part of the URL). */
static final String TARGET_GUI_ROOT = "/" + ConfigItem.GUI_IDENTIFIER.read("gui");
/** The root portion of the ChUI client request name (part of the URL). */
static final String TARGET_CHUI_ROOT = "/" + ConfigItem.CHUI_IDENTIFIER.read("chui");
/** The portion of the GUI client request name (part of the URL). */
static final String TARGET_GUI_LOGOUT = TARGET_GUI_ROOT + CONTEXT_PATH_LOGOUT;
/** The portion of the CHUI client request name (part of the URL). */
static final String TARGET_CHUI_LOGOUT = TARGET_CHUI_ROOT + CONTEXT_PATH_LOGOUT;
/** The base path for this handler. */
static final String TARGET_EMBEDDED = "/" + ConfigItem.EMBEDDED_IDENTIFIER.read("embedded");
/** Map of client uuid : authentication blob for sso. */
static final ConcurrentMap<String, String> CLIENT_UUID_AUTHBLOB_PAIRS = new ConcurrentHashMap<>();
/** Map of client uuid : session description. */
static final ConcurrentMap<String, String> CLIENT_UUID_SESS_DESCR_PAIRS = new ConcurrentHashMap<>();
/** Map of client uuid : related session ID. */
static final ConcurrentMap<String, String> CLIENT_UUID_RELATED_SESSION_ID_PAIRS = new ConcurrentHashMap<>();
/** Map of client uuid : device ID. */
static final ConcurrentMap<String, String> CLIENT_UUID_DEVICEID_PAIRS = new ConcurrentHashMap<>();
/**
* Map of client uuid : ALL login url param name : values (to be validated and filtered out before
* accessed by 4GL).
*/
static final ConcurrentMap<String, Map<String, String[]>> CLIENT_UUID_LOGIN_PARAMS = new ConcurrentHashMap<>();
/** Replaces the former WebClientBuilderParameters.cbo */
static final Map<WebDriverType, WebClientBuilderOptions> driverOptionsMap = new ConcurrentHashMap<>();
/** Template placeholder : current value pairs. Should not be updated with request specific values. */
protected static final Map<String, String> PLACEHOLDER_VALUE_PAIRS = new HashMap<>();
/** Template placeholder for page title. */
protected static final String PLACEHOLDER_TITLE = "${title}";
/** Template placeholder for the newly spawned client url. */
protected static final String AUTO_LOGIN_DATA = "${autoLoginData}";
/** Template placeholder for the html template. */
protected static final String PLACEHOLDER_HTML_TEMPLATE = "${html}";
/** Template placeholder for a reverse proxy custom context path. */
protected static final String PLACEHOLDER_PROXY_PATH = "${proxyPath}";
/** The application's package root, in folder form. */
protected static final String PKG_ROOT = Utils.getLegacyPkgRootAsJarPath();
/** The login page title. */
protected static final String PAGE_TITLE_LOGIN = ConfigItem.LOGIN_TITLE.read(null);
/** The logout page title. */
protected static final String PAGE_TITLE_LOGOUT = ConfigItem.LOGOUT_TITLE.read(null);
/**
* Flag to remove the proxy path from login/logout page urls to allow reverse proxy mapping of
* {@link WebDriverHandler#TARGET_GUI_ROOT} and
* {@link WebDriverHandler#TARGET_CHUI_ROOT} directly to the proxy path. In this case
* directory webClient configs 'serverProxyPath' and 'guiIdentifier' / 'chuiIdentifier' should be the same.
*/
protected static final boolean REMOVE_PROXY_PATH_IN_WEB_PAGES =
ConfigItem.PROXY_REMOVE_SERVER_PATH.read(false);
/** Translations manager. */
protected static final SessionlessTranslationManager TRANSLATION_MANAGER =
new SessionlessTranslationManager(WebDriverHandler.class.getSimpleName());
/** The dir name in the custom package where the relevant res reside. */
static final String PKG_RES_DIR = "webres/";
/** The reverse proxy custom context path. */
private static final String PROXY_PATH = ConfigItem.PROXY_SERVER_PATH.read(null);
/** The context for the base web resources (css, favicon, logo) */
private static final String TARGET_BASE_RES = "/webres";
/** Map of the supported file ext and their corresponding mime types. */
private static final Map<String, String> FILE_EXT_MIME_TYPE_PAIRS = new HashMap<String, String>()
{{
put("png", "image/png");
put("ico", "image/x-icon");
put("svg", "image/svg+xml");
put("css", "text/css");
put("js", "text/javascript");
}};
/** Directory config for the host:port when reverse proxy enabled. */
private static final String FORWARDED_HOST_CONFIG = ConfigItem.PROXY_HOST.read(null);
/** Directory config for the protocol when reverse proxy enabled. */
private static final String FORWARDED_PROTO_CONFIG =
ConfigItem.PROXY_PROTOCOL.read(HttpScheme.HTTPS.asString());
/** The name of the FWD device ID cookie. */
private static final String DEVICE_ID_COOKIE_NAME = "FWD-Device-ID";
/** The divider between the device ID and the signature in the FWD device ID cookie. */
private static final String DEVICE_ID_COOKIE_DIVIDER = "|";
/** Session initialization / termination listener. Kept as static field to prevent GC. */
private static final InitTermListener SESSION_LISTENER;
/** Collections of all live sessions sorted by the corresponding device ID. */
private static final Map<String, List<String>> deviceIdSessionIdPairs = new ConcurrentHashMap<>();
/** Pairs of session id (accessible from session handle) : session net id (from security manager). */
private static final Map<String, Object> sessionIdNetIdPairs = new ConcurrentHashMap<>();
/** Pairs of session id : ClientExports network interface. */
private static final Map<String, WeakReference<ClientExports>> sessionIdClientPairs =
new ConcurrentHashMap<>();
/** Map of name and validation regex for login request params configured in directory's webClient. */
private static final Map<String, Pattern> expectedLoginParamNameRegex = new ConcurrentHashMap<>();
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(WebDriverHandler.class);
/** The server's secure port or -1 if this is a remote launch. */
private static final int SECURE_PORT;
/**
* The alias of the server certificate in the certificate key store which will be used to authenticate
* that this server indeed is on the other side of the secure connection.
*/
private static final String SERVER_ALIAS;
/** Indicates the OS user for the SSO login should be overwritten by {@link #DEFAULT_OS_USER}. */
private static final boolean OS_USER_OVERRIDE = ConfigItem.OVERRIDE_OS_USER.read(false);
/** The default OS user with SSO login. */
private static final String DEFAULT_OS_USER = ConfigItem.DEFAULT_OS_USER.read(null);
/** The signature instance to sign and verify device id cookies. */
private static DigitalSignature digitalSignature;
/** The bootstrap config overrides to be applied first. Lowest priority. */
private final List<String> firstWebOverrides;
/** The bootstrap config overrides coming from directory. */
private final List<String> baseDirWebOverrides;
/** The bootstrap config overrides to be applied last. Highest priority. */
private final List<String> lastWebOverrides;
static
{
// One Browser Policy
SESSION_LISTENER = !ONE_BROWSER_POLICY ? null : new WebSessionListener();
if (SESSION_LISTENER != null)
{
StandardServer.registerSessionListener(SESSION_LISTENER);
}
// Default placeholders
PLACEHOLDER_VALUE_PAIRS.put(AUTO_LOGIN_DATA, "");
// UI Themes
List<ThemeOption> themes = LogicalTerminal.getThemes();
ObjectMapper mapper = new ObjectMapper();
ArrayNode array = mapper.createArrayNode();
if (LogicalTerminal.getRequiredTheme() == null && themes.size() > 1)
{
for (ThemeOption theme : themes)
{
ObjectNode themeNode = mapper.createObjectNode();
themeNode.put("presentationName", theme.getPresentationName());
themeNode.put("className", theme.getClassName());
themeNode.put("isDefault", theme.isDefault());
array.add(themeNode);
}
}
PLACEHOLDER_VALUE_PAIRS.put("${themes}", array.toString());
// Device ID signature
try
{
digitalSignature = DigitalSignature.fromServerKeyStore();
}
catch (Exception e)
{
LOG.severe("Couldn't instantiate DigitalSignature. Device ID will not be applicable to requests.",
e);
}
// Login page url params
List<String> expectedParams = ConfigItem.LOGIN_PARAMS.read(null);
for (String expectedParam : expectedParams)
{
String regex = ConfigItem.LOGIN_PARAMS_REGEX.read(null, expectedParam);
if (regex == null)
{
LOG.log(Level.WARNING,
"No regex found for expected login param %s. Please, check webClient configurations.",
expectedParam);
continue;
}
try
{
Pattern pattern = Pattern.compile(regex);
expectedLoginParamNameRegex.put(expectedParam, pattern);
}
catch (Throwable t)
{
String msg =
"The regex provided for login param %s is invalid. Please, check webClient configurations.";
LOG.log(Level.WARNING, t, msg, expectedParam);
}
}
// in trusted mode, we must authenticate by using the NativeSecureConnection
// to access the P2J server and get our temporary account; this requires some
// additional secure connection configuration to be available
SERVER_ALIAS = SecurityManager.getInstance().getServerAlias();
SECURE_PORT = SessionManager.get().config().getInt(ConfigItem.SERVER_SECURE_PORT, -1);
// Directory-derived options
if (ConfigItem.VIRTUAL_DESKTOP.read(false))
{
driverOptionsMap.put(WebDriverType.CHUI, new WebClientBuilderOptions(WebDriverType.CHUI));
driverOptionsMap.put(WebDriverType.GUI, new WebClientBuilderOptions(WebDriverType.GUI));
}
if (ConfigItem.EMBEDDED.read(false))
{
driverOptionsMap.put(WebDriverType.EMBEDDED, new WebClientBuilderOptions(WebDriverType.EMBEDDED));
}
}
/**
* Package-private constructor. Reads web client config overrides and creates an options list.
*
* @param firstOption
* An additional option to be added at the start of the options. Can be overwritten.
* @param lastOption
* An additional option to be added at the end of the options. Should not be overwritten.
*/
WebDriverHandler(String firstOption, String lastOption)
{
firstWebOverrides = firstOption != null ?
Collections.singletonList(firstOption) :
Collections.emptyList();
baseDirWebOverrides = Arrays.asList(ConfigItem.CFG_OVERRIDES_WEB.read("").split(" "));
lastWebOverrides = lastOption != null ?
Collections.singletonList(lastOption) :
Collections.emptyList();
}
/**
* Returns a list of session ids for the specified device id.
*
* @param deviceId
* The device id.
*
* @return List of session ids for the specified device id.
*/
public static List<String> getSessionsByDeviceId(String deviceId)
{
return deviceIdSessionIdPairs.get(deviceId);
}
/**
* Returns the session net id for the specified session id.
*
* @param sessionId
* The session id (as in the session handle).
*
* @return The session net id.
*/
public static Object getSessionNetId(String sessionId)
{
return sessionIdNetIdPairs.get(sessionId);
}
/**
* Returns the network interface of the client.
*
* @param sessionId
* The session id (as in the session handle).
*
* @return The network interface of the client.
*/
public static ClientExports getSessionClientInterface(String sessionId)
{
return sessionIdClientPairs.get(sessionId).get();
}
/**
* Returns the proxy path as absolute path if configured in directory, otherwise an empty string.
*
* @param request
* The HTTP request.
*
* @return See above.
*/
public static String getServerProxyPath(HttpServletRequest request)
{
return getServerProxyPath(resolveForwardedHost(request));
}
/**
* Returns the proxy path as absolute path if configured in directory, otherwise an empty string.
*
* @param forwardedHost
* The proxy address.
*
* @return See above.
*/
public static String getServerProxyPath(String forwardedHost)
{
if (!StringHelper.hasContent(forwardedHost))
{
// no proxy
return "";
}
if (!StringHelper.hasContent(PROXY_PATH))
{
// no config
return "";
}
return "/" + PROXY_PATH;
}
/**
* Returns a map of all valid login params according to directory configurations.
*
* @param loginParams
* The map of name : values for the params sent by the login request.
*
* @return See above.
*/
public static Map<String, String[]> filterOutValidLoginParams(Map<String, String[]> loginParams)
{
Map<String, String[]> validatedParams = new HashMap<>();
for (String expectedParamName : expectedLoginParamNameRegex.keySet())
{
String[] actualValues = loginParams.get(expectedParamName);
if (actualValues == null)
{
continue;
}
Pattern expectedParamPattern = expectedLoginParamNameRegex.get(expectedParamName);
List<String> validatedValues = new ArrayList<>();
for (int valueIndex = 0; valueIndex < actualValues.length; valueIndex++)
{
String actualValue = actualValues[valueIndex];
if (!expectedParamPattern.matcher(actualValue).matches())
{
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER,
"A successful login request for FWD user %s has invalid value '%s' for param '%s'.",
SecurityManager.getInstance().getUserId(),
actualValue,
expectedParamName);
}
continue;
}
validatedValues.add(actualValue);
}
if (!validatedValues.isEmpty())
{
validatedParams.put(expectedParamName, validatedValues.toArray(new String[0]));
}
}
return validatedParams;
}
/**
* Handle HTTP requests for common resources (css style, favicon and logo image).
*
* @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;
}
target = target.toLowerCase();
if (target.startsWith(TARGET_BASE_RES) && request.getMethod().toUpperCase().equals("GET"))
{
base.setHandled(true);
String fileName = target.replaceFirst("/" + PKG_RES_DIR, "");
String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);
String mimeType = FILE_EXT_MIME_TYPE_PAIRS.getOrDefault(fileExt, "text/plain");
Map<String, String> placeholderValuePairs = fileExt.equals("js") ?
new HashMap<String, String>(PLACEHOLDER_VALUE_PAIRS) {{
put(UiTextKey.MSG_SDK_DEFAULT_REQUEST_ERROR.name(),
TRANSLATION_MANAGER.translate(UiTextKey.MSG_SDK_DEFAULT_REQUEST_ERROR.name()));
}} :
PLACEHOLDER_VALUE_PAIRS;
serveStatic(response,
mimeType,
placeholderValuePairs,
PKG_ROOT + PKG_RES_DIR + fileName,
getResPathCurrentDir(fileName));
}
}
catch (Throwable t)
{
LOG.warning("Unexpected exception:", t);
sendError(response, SupportedHttpCode.UNEXPECTED_ERROR, null);
}
}
/**
* Handles HTTP requests for spawning a new client.
*
* @param base
* The HTTP request, Jetty style.
* @param request
* The HTTP request.
* @param response
* The HTTP response.
* @param isSso
* Flag indicating if SSO is enabled.
* @param driverType
* The type of web client - GUI / CHUI / embedded.
*/
protected void handleStartClientRequest(Request base,
HttpServletRequest request,
HttpServletResponse response,
boolean isSso,
WebDriverType driverType)
{
WebDriverRequestParameters requestParameters = getRequestParameters(base, request);
String deviceId = setDeviceId(requestParameters, response);
List<String> options = addThemeToOptions(requestParameters.getTheme());
options = addUrlsToOptions(options, requestParameters, driverType);
WebDriverSpawnParameters spawnParams = new WebDriverSpawnParameters(driverType,
isSso,
options,
deviceId);
SpawnerResult spawnerResult = spawnWorker(requestParameters, spawnParams);
if (spawnerResult.uri == null)
{
sendError(response, spawnerResult.rc, spawnerResult.spawnError);
return;
}
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO,
"Returns the redirect url %s as response to %s client.",
spawnerResult.uri,
driverType);
}
writeUrlInResponse(spawnerResult.uri,
spawnerResult.storageId,
response,
spawnerResult.webClientUuid,
spawnerResult.cookie);
}
/**
* Retrieves the FWD device ID cookie and resets the expiration date. If not present, generates a new
* device ID and adds the cookie to the response. Returns the device ID.
*
* @param requestParameters
* Holds HTTP derived parameters - headers, cookies, form params.
* @param response
* The HTTP response.
*
* @return The device ID.
*/
protected String setDeviceId(WebDriverRequestParameters requestParameters, HttpServletResponse response)
{
Cookie[] cookies = requestParameters.getCookies();
Tuple<String, Cookie> deviceIdCookiePair = getValidDeviceId(cookies, requestParameters.getUser());
deviceIdCookiePair = deviceIdCookiePair == null ? generateDeviceId() : deviceIdCookiePair;
if (deviceIdCookiePair == null)
{
return null;
}
Cookie deviceIdCookie = deviceIdCookiePair.getValue();
setDeviceIdCookieAttributes(deviceIdCookie);
response.addCookie(deviceIdCookie);
return deviceIdCookiePair.getKey();
}
/**
* Spawn a client with the given account credentials and UI mode.
*
* @param requestParameters
* Holds HTTP derived parameters - headers, cookies, form params.
* @param spawnParameters
* Holds additional parameters required for the spawn process.
*
* @return The redirect URI will be populated if successful, otherwise the URI will be
* <code>null</code> and the return code will describe the error.
*/
protected SpawnerResult spawnWorker(WebDriverRequestParameters requestParameters,
WebDriverSpawnParameters spawnParameters)
{
SecurityManager sm = SecurityManager.getInstance();
SpawnerResult result = new SpawnerResult();
result.rc = SupportedHttpCode.UNEXPECTED_ERROR;
String user = requestParameters.getUser();
String pw = requestParameters.getPassword();
String authBlob = spawnParameters.isSessionFork() ?
spawnParameters.getSessionForkParams().getAuthBlob() :
null;
String fwdUser = spawnParameters.isSessionFork() ?
spawnParameters.getSessionForkParams().getFwdUser() :
null;
String storageId = spawnParameters.isSessionFork() ?
spawnParameters.getSessionForkParams().getStorageId() :
null;
String sessionDescription = spawnParameters.isSessionFork() ?
"Forked session" :
null;
if (!spawnParameters.isSessionFork() && spawnParameters.isSso())
{
if (spawnParameters.getSsoAuthResult() != null)
{
// auto login has already completed
authBlob = spawnParameters.getSsoAuthResult().getAuthBlob();
fwdUser = spawnParameters.getSsoAuthResult().getFwdUser();
storageId = spawnParameters.getSsoAuthResult().getPersistentUserStorageIdentifier();
sessionDescription = spawnParameters.getSsoAuthResult().getSessionDescription();
}
else
{
SsoAuthenticator ssoAuthenticator = sm.ssoSm.getSsoAuthenticator();
if (ssoAuthenticator == null)
{
result.spawnError = TRANSLATION_MANAGER.translate(UiTextKey.MSG_AUTHENTICATOR_ISSUE.name());
LOG.warning(result.spawnError);
return result;
}
SsoAuthenticator.Result ssoAuthResult =
ssoAuthenticator.authenticate(requestParameters.getParamMap(),
requestParameters.getCookies(),
new SsoAuthenticator.LicensingData(
requestParameters.getExternalUserIp(),
requestParameters.getInternalUserIp(),
spawnParameters.getDeviceId()));
if (!ssoAuthResult.isSuccess())
{
result.rc = ssoAuthResult.getFailureHttpCode() != null ?
ssoAuthResult.getFailureHttpCode() :
SupportedHttpCode.UNAUTHORIZED;
result.spawnError = ssoAuthResult.getTranslatedErrorMessage() != null ?
ssoAuthResult.getTranslatedErrorMessage() :
TRANSLATION_MANAGER.translate(UiTextKey.MSG_INVALID_CREDENTIALS.name());
LOG.fine("SSO authentication failed. " + result.spawnError);
return result;
}
SsoUsers ssoUsers = processSsoResult(ssoAuthResult, sm, result);
if (ssoUsers == null)
{
LOG.warning(result.spawnError);
result.osUser = "";
return result;
}
LOG.log(Level.FINE, "Attempting spawn for SSO authenticated app user %s.", user);
authBlob = ssoAuthResult.getAuthBlob();
storageId = ssoAuthResult.getPersistentUserStorageIdentifier();
// replace in-app user & pass with os user & pass
user = ssoUsers.osUser;
pw = ssoUsers.osPass;
fwdUser = ssoUsers.fwdUser;
sessionDescription = ssoAuthResult.getSessionDescription();
}
// the override flag allows the server to force the OS user to a specific value
// honor default OS user if user was not specified
if (OS_USER_OVERRIDE || user == null)
{
if (DEFAULT_OS_USER == null)
{
result.spawnError =
String.format(TRANSLATION_MANAGER.translate(UiTextKey.MSG_DEFAULT_OS_USER_MISSING.name()), OS_USER_OVERRIDE);
LOG.warning(result.spawnError);
return result;
}
String[] osUserCred = sm.getOsUserCredByName(DEFAULT_OS_USER);
if (osUserCred == null)
{
result.spawnError = String.format(TRANSLATION_MANAGER.translate(UiTextKey.MSG_OSUSER_NOT_LISTED.name()),
DEFAULT_OS_USER);
LOG.warning(result.spawnError);
return result;
}
user = osUserCred[0];
pw = osUserCred[1];
}
}
result.osUser = user;
LOG.log(Level.FINE, "Attempting spawn under OS user %s.", user);
pw = StringHelper.hasContent(pw) ? pw : null;
boolean trusted = pw == null;
WebClientBuilderParameters builderParams =
new WebClientBuilderParameters(user,
pw,
driverOptionsMap.get(spawnParameters.getDriverType()),
SECURE_PORT,
SERVER_ALIAS,
trusted,
fwdUser,
storageId,
firstWebOverrides,
baseDirWebOverrides,
spawnParameters.getRuntimeWebOptions(),
lastWebOverrides);
WebClientsManager webClientsManager = WebClientsManager.getInstance();
WebClientSpawner spawner = new WebClientSpawner(builderParams, webClientsManager);
String clientUuid = builderParams.getUuid();
if (authBlob != null)
CLIENT_UUID_AUTHBLOB_PAIRS.put(clientUuid, authBlob);
if (fwdUser != null)
CLIENT_UUID_FWD_USERS.put(clientUuid, fwdUser);
if (spawnParameters.isSessionFork())
CLIENT_UUID_RELATED_SESSION_ID_PAIRS.put(clientUuid,
spawnParameters.getSessionForkParams().getRelatedSessionId());
if (sessionDescription != null)
CLIENT_UUID_SESS_DESCR_PAIRS.put(clientUuid, sessionDescription);
CLIENT_UUID_DEVICEID_PAIRS.put(clientUuid, spawnParameters.getDeviceId());
CLIENT_UUID_LOGIN_PARAMS.put(clientUuid, requestParameters.getParamMap());
int exitCode = spawner.spawn(webClientsManager,
new String[] {
requestParameters.getForwardedHost(),
requestParameters.getForwardedProto(),
requestParameters.getExternalUserIp()});
if (exitCode == 0)
{
try
{
result.uri = spawner.getRemoteUri();
result.storageId = storageId;
result.rc = SupportedHttpCode.SUCCESS;
}
catch (URISyntaxException e)
{
LOG.log(Level.SEVERE,
e,
"Rewrite failed: X_FORWARDED_HOST=%s X_FORWARDED_PROTO=%s",
requestParameters.getForwardedHost(),
requestParameters.getForwardedProto());
}
}
if (result.uri == null)
{
CLIENT_UUID_AUTHBLOB_PAIRS.remove(clientUuid);
CLIENT_UUID_FWD_USERS.remove(clientUuid);
CLIENT_UUID_SESS_DESCR_PAIRS.remove(clientUuid);
CLIENT_UUID_RELATED_SESSION_ID_PAIRS.remove(clientUuid);
CLIENT_UUID_DEVICEID_PAIRS.remove(clientUuid);
CLIENT_UUID_LOGIN_PARAMS.remove(clientUuid);
SpawnError spawnError = SpawnError.get(exitCode).orElse(SpawnError.DEFAULT_SPAWN_ERR);
result.spawnError = String.format(TRANSLATION_MANAGER.translate(spawnError.name()), exitCode);
if (result.rc == SupportedHttpCode.SUCCESS || result.rc == null)
{
result.rc = !spawnParameters.isSso() && spawnError.isInvalidOsCredentials() ?
SupportedHttpCode.UNAUTHORIZED :
SupportedHttpCode.UNEXPECTED_ERROR;
}
}
result.webClientUuid = clientUuid;
return result;
}
/**
* Handles HTTP requests that allow SSO auto-login and direct spawn of a new client.
*
* @param requestParameters
* Holds HTTP derived parameters - headers, cookies, form params.
* @param deviceId
* The device ID.
* @param driverType
* The type of web client - GUI / CHUI / embedded.
*
* @return The result of the spawn attempt.
*/
protected SpawnerResult handleSsoAutoLogin(WebDriverRequestParameters requestParameters,
String deviceId,
WebDriverType driverType)
{
Cookie[] cookies = requestParameters.getCookies();
if (cookies == null || cookies.length <= 0)
{
return null;
}
SecurityManager sm = SecurityManager.getInstance();
SsoAuthenticator ssoAuthenticator = sm.ssoSm.getSsoAuthenticator();
if (ssoAuthenticator == null)
{
return null;
}
String authCookieName = ssoAuthenticator.getAuthCookieName();
if (authCookieName == null)
{
return null;
}
boolean hasAuthCookie = false;
for (int i = 0; i < cookies.length; i++)
{
Cookie cookie = cookies[i];
if (cookie.getName().equals(authCookieName))
{
hasAuthCookie = true;
break;
}
}
if (!hasAuthCookie)
{
return null;
}
SsoAuthenticator.Result ssoAuthResult =
ssoAuthenticator.authenticate(cookies,
new SsoAuthenticator.LicensingData(requestParameters.getExternalUserIp(),
requestParameters.getInternalUserIp(),
deviceId));
if (ssoAuthResult == null || !ssoAuthResult.isSuccess())
{
return null;
}
SpawnerResult result = new SpawnerResult();
SsoUsers ssoUsers = processSsoResult(ssoAuthResult, sm, result);
if (ssoUsers == null)
{
return null;
}
requestParameters.setUser(ssoUsers.osUser);
requestParameters.setPassword(ssoUsers.osPass);
List<String> options = addThemeToOptions(requestParameters.getTheme());
options = addUrlsToOptions(options, requestParameters, driverType);
WebDriverSpawnParameters spawnParams = new WebDriverSpawnParameters(driverType,
true,
options,
deviceId);
spawnParams.setSsoAuthResult(ssoAuthResult);
SpawnerResult spawnerResult = spawnWorker(requestParameters, spawnParams);
if (spawnerResult.rc != SupportedHttpCode.SUCCESS)
{
return null;
}
if (spawnerResult.uri == null)
{
LOG.fine("No URL returned by the spawnWorker method.");
return null;
}
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO, "Client to be redirected by auto-login to %s.", spawnerResult.uri);
}
return spawnerResult;
}
/**
* Handles HTTP requests that allow forking session and direct spawn of a new client.
*
* @param requestParameters
* Holds HTTP derived parameters - headers, cookies, form params.
* @param sm
* SecurityManager
* @param driver
* The UI driver type
* @param isSso
* Flag indicating if SSO is enabled
* @param deviceId
* The device ID
* @param placeholderValuePairs
* Map with template placeholder : value pairs
*
* @return The result of the spawn attempt.
*/
protected boolean handleSessionFork(WebDriverRequestParameters requestParameters,
SecurityManager sm,
WebDriverType driver,
boolean isSso,
String deviceId,
Map<String, String> placeholderValuePairs)
{
String oneTimeToken = requestParameters.getParameter(SessionForkManager.PARAM_NAME_TOKEN);
if (!StringHelper.hasContent(oneTimeToken))
{
return false;
}
SessionForkManager.ForkedSessionConfigs sessionForkConfigs =
SessionForkManager.getInstance().getConfigsForToken(oneTimeToken);
if (sessionForkConfigs == null)
{
LOG.info("Attempt to start a new forked session with invalid or expired token from end-user with " +
"IP: " + requestParameters.getExternalUserIp());
return false;
}
String osUser = sessionForkConfigs.getOsUser();
String[] osUserCred = sm.getOsUserCredByName(osUser);
if (osUserCred != null && osUserCred.length == 2)
{
requestParameters.setUser(osUserCred[0]);
requestParameters.setPassword(osUserCred[1]);
}
else
{
requestParameters.setUser(osUser);
}
List<String> options = new ArrayList<>();
options = addUrlsToOptions(options, requestParameters, driver);
if (sessionForkConfigs.getCfgOverrides() != null)
{
options.addAll(sessionForkConfigs.getCfgOverrides());
}
WebDriverSpawnParameters spawnParams = new WebDriverSpawnParameters(driver,
isSso,
options,
deviceId);
spawnParams.setSessionFork(sessionForkConfigs);
SpawnerResult spawnerResult = spawnWorker(requestParameters, spawnParams);
if (spawnerResult.uri == null)
{
return false;
}
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO,
"Forked session. Returns the redirect url %s as response to %s client.",
spawnerResult.uri,
driver);
}
// the new client url will be injected in the landing page as template content
placeholderValuePairs.put(AUTO_LOGIN_DATA,
new ObjectMapper().createObjectNode()
.put("url", spawnerResult.uri)
.put("storageId", spawnerResult.storageId)
.toString());
return true;
}
/**
* Loads static resources, inflates templates and serves the content. If the resource is not found in
* any of the alternative paths provided, or if IOException occurs, the status is set to 404, otherwise 200.
*
* @param response
* The HTTP response.
* @param requestSpecificTemplateValues
* The template placeholder values.
* @param altPaths
* The target resource paths within the classpath.
*/
protected void serveHtml(HttpServletResponse response,
Map<String, String> requestSpecificTemplateValues,
String... altPaths)
{
Map<String, String> allPlaceholderValues = new HashMap<>();
allPlaceholderValues.putAll(PLACEHOLDER_VALUE_PAIRS);
if (requestSpecificTemplateValues != null)
{
allPlaceholderValues.putAll(requestSpecificTemplateValues);
}
serveStatic(response, MimeTypes.Type.TEXT_HTML_UTF_8.toString(), allPlaceholderValues, altPaths);
}
/**
* Loads static resource, inflates templates and serves the content. If the resource is not found in any
* of the alternative paths provided, or if IOException occurs, the status is set to 404, otherwise 200.
*
* @param response
* The HTTP response.
* @param mimeType
* The mime type.
* @param placeholderValues
* The template placeholder values.
* @param altPaths
* The target resource paths within the classpath.
*/
protected void serveStatic(HttpServletResponse response,
String mimeType,
Map<String, String> placeholderValues,
String... altPaths)
{
boolean resFound = false;
for (String path : altPaths)
{
InputStream inputStream = getClass().getResourceAsStream(path);
if (inputStream == null)
{
continue;
}
resFound = true;
response.setContentType(mimeType);
if (mimeType.startsWith("image"))
{
writeBinaryResponse(response, inputStream, path);
}
else
{
writeTextResponse(response, inputStream, path, placeholderValues);
}
break;
}
if (resFound)
{
setResponseSuccess(response);
}
else
{
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Reads the input stream line by line, replaces placeholders and serves the content. If IOException
* occurs, the status is set to 404, otherwise 200.
*
* @param response
* The HTTP response.
* @param inputStream
* The text file input stream.
* @param path
* The text resource file path.
* @param placeholderValues
* The template placeholder values.
*/
private void writeTextResponse(HttpServletResponse response,
InputStream inputStream,
String path,
Map<String, String> placeholderValues)
{
try (Scanner scanner = new Scanner(inputStream))
{
while (scanner.hasNext())
{
String line = scanner.nextLine();
if (placeholderValues != null)
{
line = inflateTemplate(line, placeholderValues);
}
response.getWriter().println(line);
}
}
catch (IOException e)
{
LOG.log(Level.FINE, e, "Couldn't serve static resource %s.", path);
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Reads the input stream a few bytes at a time and serves the content. If IOException occurs, the
* status is set to 404, otherwise 200.
*
* @param response
* The HTTP response.
* @param inputStream
* The text file input stream.
* @param path
* The text resource file path.
*/
private void writeBinaryResponse(HttpServletResponse response, InputStream inputStream, String path)
{
try
{
byte[] buffer = new byte[1024];
int bytesRead;
OutputStream outputStream = response.getOutputStream();
while ((bytesRead = inputStream.read(buffer)) != -1)
{
outputStream.write(buffer, 0, bytesRead);
}
}
catch (IOException e)
{
LOG.log(Level.FINE, e, "Couldn't serve static resource %s.", path);
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
/**
* Returns the HTML partial resource as string. First tries to find the resource under the custom
* package dir, then looks for it under the current package.
*
* @param partialFilename
* The name of the file
* @param customPackageDir
* The dir in the custom package
*
* @return String representation of the HTML partial resource
*/
protected String loadPartial(String partialFilename,
String customPackageDir,
Map<String, String> placeholderValuePairs)
{
InputStream customResInputStream = getClass()
.getResourceAsStream(PKG_ROOT + customPackageDir + partialFilename);
InputStream resInputStream = customResInputStream != null ?
customResInputStream :
getClass().getResourceAsStream(getResPathCurrentDir(partialFilename));
if (resInputStream == null)
{
return null;
}
StringBuilder partialBuilder = new StringBuilder();
try (Scanner scanner = new Scanner(resInputStream))
{
HashMap<String, String> placeholderValues = new HashMap<>(PLACEHOLDER_VALUE_PAIRS);
if (placeholderValuePairs != null)
{
placeholderValues.putAll(placeholderValuePairs);
}
while (scanner.hasNext())
{
partialBuilder.append(inflateTemplate(scanner.nextLine(), placeholderValues))
.append("\n");
}
}
return partialBuilder.toString();
}
/**
* Returns the web context path to the target resource file, transforming the class package into segments
* of the path.
*
* @param fileName
* The name of the file
*
* @return String representation of the client's IP address
*/
protected String getResPathCurrentDir(String fileName)
{
return "/" + getClass().getPackage().getName().replace('.', '/') + "/" + fileName;
}
/**
* Replaces placeholders in the text with the provided values.
*
* @param line
* The text to be updated.
* @param placeholderValues
* The map with placeholder name : value pairs.
*
* @return The text with replaced placeholders.
*/
private String inflateTemplate(String line, Map<String, String> placeholderValues)
{
for (String placeholder : placeholderValues.keySet())
{
if (line.contains(placeholder))
{
line = line.replace(placeholder, placeholderValues.get(placeholder));
}
}
return line;
}
/**
* Processes the successful SSO login result. Finds the OS user associated with the FWD account. Sets
* the cookies to the spawner result.
*
* @param ssoAuthResult
* The result of the SSO authentication.
* @param sm
* The SecurityManager.
* @param result
* The result from the new client spawn.
*
* @return The container for accounts associated with the sso login.
*/
private static SsoUsers processSsoResult(SsoAuthenticator.Result ssoAuthResult,
SecurityManager sm,
SpawnerResult result)
{
String fwdUser = ssoAuthResult.getFwdUser();
if (!StringHelper.hasContent(fwdUser))
{
result.spawnError =
String.format(TRANSLATION_MANAGER.translate(UiTextKey.MSG_INVALID_FWD_ACCOUNT.name()), fwdUser);
LOG.warning(result.spawnError);
return null;
}
result.cookie = ssoAuthResult.getCookie();
String osUser = null;
String osPass = null;
if (ssoAuthResult.getOsUser() != null)
{
osUser = ssoAuthResult.getOsUser();
osPass = ssoAuthResult.getOsPassword();
}
else
{
String[] osUserCred = sm.getOsUserCredByFwdAcc(fwdUser);
// these can be overridden by configs
osUser = osUserCred == null ? null : osUserCred[0];
osPass = osUserCred == null || osUserCred.length < 2 ? null : osUserCred[1];
}
return new SsoUsers(fwdUser, osUser, osPass);
}
/**
* Retrieves these parameters: a forwarded host, a forwarded proto and a client IP address if
* they are provided by http request.
*
* @param base
* The HTTP request. Jetty style.
* @param request
* The HTTP servlet request
*
* @return The parameters array is filled that the first parameters[0] holds the forwarded
* host of "X-Forwarded-Host" HTTP header, the second parameters[1] holds the used
* forwarded protocol that is provided by "X-Forwarded-Proto" value and the third
* parameters[2] holds the client IP address.
*/
static WebDriverRequestParameters getRequestParameters(Request base, HttpServletRequest request)
{
String externalUserIp;
String internalUserIp;
String forwardedIps = request.getHeader(HttpHeader.X_FORWARDED_FOR.asString());
if (StringHelper.hasContent(forwardedIps))
{
String[] forwardedIpArray = forwardedIps.split(",");
externalUserIp = forwardedIpArray[0];
// same as HttpHeader.X-Real-IP
internalUserIp = forwardedIpArray[forwardedIpArray.length - 1];
}
else
{
externalUserIp = request.getRemoteAddr();
internalUserIp = request.getRemoteAddr();
}
return new WebDriverRequestParameters()
.setForwardedHost(resolveForwardedHost(request))
.setForwardedProto(FORWARDED_HOST_CONFIG != null ?
FORWARDED_PROTO_CONFIG :
request.getHeader(HttpHeader.X_FORWARDED_PROTO.asString()))
.setExternalUserIp(externalUserIp)
.setInternalUserIp(internalUserIp)
.setServerName(request.getServerName())
.setServerPort(request.getServerPort())
.setCookies(request.getCookies())
.setParamMap(request.getParameterMap())
.setQueryParams(base.getQueryParameters())
.setUser(base.getParameter("usr"))
.setPassword(base.getParameter("psw"))
.setTheme(base.getParameter("theme"));
}
/**
* Returns the resolved forward host.
*
* @param request
* The HTTP servlet request
*
* @return See above.
*/
static String resolveForwardedHost(HttpServletRequest request)
{
return FORWARDED_HOST_CONFIG != null ?
FORWARDED_HOST_CONFIG :
request.getHeader(HttpHeader.X_FORWARDED_HOST.asString());
}
/**
* Sets status, content type and headers for a successful response.
*
* @param response
* The HTTP response.
*/
private void setResponseSuccess(HttpServletResponse response)
{
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(HttpHeader.CACHE_CONTROL.asString(), "no-cache, no-store, must-revalidate");
response.setDateHeader(HttpHeader.EXPIRES.asString(), 0);
}
/**
* Sends the error message to the client.
*
* @param httpCode
* The http code to return to the client.
* @param response
* The http response.
*/
void sendError(HttpServletResponse response, SupportedHttpCode httpCode, String spawnError)
{
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
response.setStatus(httpCode.getStatus());
response.setHeader(HttpHeader.CACHE_CONTROL.asString(), "no-cache, no-store, must-revalidate");
response.setDateHeader(HttpHeader.EXPIRES.asString(), 0);
try
{
PrintWriter writer = response.getWriter();
writer.println(spawnError != null ? spawnError : SpawnError.GENERIC.getMsg());
writer.flush();
}
catch (IOException ioe)
{
LOG.severe("Couldn't send an error as response!", ioe);
}
}
/**
* Returns the url to the main application site (the new web session) as response.
*
* @param remoteUri
* The redirect url provided with the authorization parameter.
* @param storageId
* The storage ID.
* @param response
* The HTTP response.
* @param webClientUuid
* The just spawned web client uuid.
* @param cookie
* The cookie to be attached to the response.
*/
private void writeUrlInResponse(String remoteUri,
String storageId,
HttpServletResponse response,
String webClientUuid,
Cookie cookie)
{
setResponseSuccess(response);
response.setContentType(MimeTypes.Type.TEXT_HTML.asString());
if (cookie != null)
{
response.addCookie(cookie);
}
ObjectMapper mapper = new ObjectMapper();
ObjectNode responseJson = mapper.createObjectNode();
responseJson.put("url", remoteUri);
responseJson.put("storageId", storageId);
try
{
PrintWriter writer = response.getWriter();
writer.println(responseJson.toString());
writer.flush();
}
catch (IOException e)
{
WebClientsManager.getInstance().freeWebClientResources(webClientUuid);
LOG.log(Level.WARNING, "A new client has been spawned with auto login, but the url couldn't " +
"get returned to the browser.", e);
}
}
/**
* Adds the chosen theme from the HTTP request parameters to the spawn options. If a required theme is
* configured in directory, the theme selected in the UI (sent via HTTP request) is discarded.
*
* @param selectedTheme
* The theme from the HTTP request parameters.
*
* @return A modifiable list of all base options + the selected theme.
*/
private static List<String> addThemeToOptions(String selectedTheme)
{
List<String> options = new ArrayList<>();
if (LogicalTerminal.getRequiredTheme() != null)
{
return options;
}
if (StringHelper.hasContent(selectedTheme) && LogicalTerminal.isValidThemeClass(selectedTheme))
{
options.add("client:driver:theme=" + selectedTheme);
}
return options;
}
/**
* Adds the urls for login and logout page to list of spawn options.
*
* @param options
* The current client options.
* @param requestParameters
* The web request details fetched from the servlet request.
* @param driverType
* The type of web client - GUI / CHUI / embedded.
*
* @return A modifiable list of all base options + the login and logout urls.
*/
private static List<String> addUrlsToOptions(List<String> options,
WebDriverRequestParameters requestParameters,
WebDriverType driverType)
{
List<String> modifiableOptions = new ArrayList<>(options);
boolean hasProxy = StringHelper.hasContent(requestParameters.getForwardedHost());
String proxyPath = hasProxy ? getServerProxyPath(requestParameters.getForwardedHost()) : "";
String baseUrl = hasProxy ?
requestParameters.getForwardedProto() + "://" + requestParameters.getForwardedHost() :
HttpScheme.HTTPS.asString() + "://" + requestParameters.getServerName() + ":" + requestParameters.getServerPort();
String loginPage =
baseUrl + (REMOVE_PROXY_PATH_IN_WEB_PAGES ? "" : proxyPath) + driverType.getLoginTarget();
String logoutPage =
baseUrl + (REMOVE_PROXY_PATH_IN_WEB_PAGES ? "" : proxyPath) +
(isSsoEnabled() ? driverType.getLogoutTarget() : driverType.getLoginTarget());
MultiMap<String> queryParams = requestParameters.getQueryParams();
queryParams.remove(SessionForkManager.PARAM_NAME_TOKEN);
if (!queryParams.isEmpty())
{
String encodedQueryString = UrlEncoded.encode(queryParams, StandardCharsets.UTF_8, false);
logoutPage += "?" + encodedQueryString;
}
modifiableOptions.add("web:url:loginPage=" + loginPage);
modifiableOptions.add("web:url:logoutPage=" + logoutPage);
modifiableOptions.add("web:url:serverBaseUrl=" + baseUrl + proxyPath);
return modifiableOptions;
}
/**
* Returns a flag indicating if SSO is enabled.
*
* @return <code>true</code> if SSO enabled, <code>false</code> otherwise.
*/
protected static boolean isSsoEnabled()
{
return SecurityManager.getInstance().ssoSm.isSsoEnabled();
}
/**
* Searches through all cookies in the request and returns boolean to indicate if a valid device ID
* cookie is found.
*
* @param requestCookies
* Array of all cookies in the request.
* @param user
* The name of the user trying to log in.
*
* @return Tuple with the device ID and the FWD device ID cookie.
*/
private Tuple<String, Cookie> getValidDeviceId(Cookie[] requestCookies, String user)
{
Tuple<String, Cookie> validDeviceIdCookiePair = null;
if (requestCookies == null)
{
return null;
}
for (int i = 0; i < requestCookies.length; i++)
{
Cookie cookie = requestCookies[i];
// ideally we should add checks for HttpOnly & Secure, but the fields are not set by the container
if (cookie.getName().equals(DEVICE_ID_COOKIE_NAME))
{
String value = cookie.getValue();
int dividerIndex = value.indexOf(DEVICE_ID_COOKIE_DIVIDER);
if (dividerIndex == -1)
{
continue;
}
String deviceId = value.substring(0, dividerIndex);
String signatureHex = value.substring(dividerIndex + 1);
if (!StringHelper.hasContent(deviceId) || !StringHelper.hasContent(signatureHex))
{
continue;
}
byte[] signatureBytes = DigitalSignature.fromHex(signatureHex);
try
{
if (digitalSignature.verify(deviceId, signatureBytes))
{
validDeviceIdCookiePair = new Tuple<>(deviceId, cookie);
break;
}
}
catch (Exception e)
{
LOG.info(DEVICE_ID_COOKIE_NAME + " cookie found with invalid signature for user " + user, e);
}
}
}
return validDeviceIdCookiePair;
}
/**
* Generates a new UUID for a new device ID, signs it with the server certificate and sets it in a cookie.
*
* @return Tuple with the device ID and the FWD device ID cookie.
*/
private Tuple<String, Cookie> generateDeviceId()
{
String deviceId = UUID.randomUUID().toString();
try
{
byte[] signatureBytes = digitalSignature.sign(deviceId);
String signature = DigitalSignature.toHex(signatureBytes);
Cookie cookie = new Cookie(DEVICE_ID_COOKIE_NAME, deviceId + DEVICE_ID_COOKIE_DIVIDER + signature);
setDeviceIdCookieAttributes(cookie);
return new Tuple<>(deviceId, cookie);
}
catch (Throwable e)
{
LOG.info("Unable to generate device id.", e);
}
return null;
}
/**
* Sets attributes for the device ID cookie
*
* @param deviceIdCookie
* The device id cookie.
*/
private void setDeviceIdCookieAttributes(Cookie deviceIdCookie)
{
deviceIdCookie.setHttpOnly(true);
deviceIdCookie.setSecure(true);
deviceIdCookie.setMaxAge(Integer.MAX_VALUE); // 400 days is the max age supported by Chrome
}
/** Enum for result codes available to the end-user. */
public enum SupportedHttpCode
{
/** 200 Success. */
SUCCESS(HttpServletResponse.SC_OK),
/** 401 Unauthorized. */
UNAUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED),
/** 403 Forbidden. */
FORBIDDEN(HttpServletResponse.SC_FORBIDDEN),
/** 500 Internal Server Error. */
UNEXPECTED_ERROR(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
/** The HTTP code to be returned to the end-user. */
private final int status;
/**
* Contructor to set the message for that specific enum value.
*
* @param status
* The HTTP code.
*/
SupportedHttpCode(int status)
{
this.status = status;
}
/**
* Getter for {@link #status}.
*
* @return See above.
*/
public int getStatus()
{
return status;
}
}
/** Simple container for accounts associated with the sso login. */
protected static final class SsoUsers
{
/** FWD account. */
protected final String fwdUser;
/** OS user ID. */
protected final String osUser;
/** OS user password. */
protected final String osPass;
/**
* Private constructor.
*
* @param fwdUser
* FWD account
* @param osUser
* OS user ID
* @param osPass
* OS user password
*/
private SsoUsers(String fwdUser, String osUser, String osPass)
{
this.fwdUser = fwdUser;
this.osUser = osUser;
this.osPass = osPass;
}
}
/** Simple container for the spawn result. */
static class SpawnerResult
{
/** Redirect URI in case of a successful spawn. <code>null</code> on failure. */
public String uri;
/** Unique identifier for the local storage used by the user. */
public String storageId;
/** The uuid of the spawned client. */
public String webClientUuid;
/** Cookie. */
public Cookie cookie;
/** Result code. */
public SupportedHttpCode rc;
/** Spawn error. */
public String spawnError;
/** OS username. */
public String osUser;
}
/** Keys for translation resources. */
private enum UiTextKey
{
/** Error message format for invalid FWD account returned by the authenticator. */
MSG_INVALID_FWD_ACCOUNT,
/** Error message format for user not listed in osusers */
MSG_OSUSER_NOT_LISTED,
/** Error message format for missing defaultOsUser */
MSG_DEFAULT_OS_USER_MISSING,
/** Error message format for invalid in-app credentials */
MSG_INVALID_CREDENTIALS,
/** Error message format for issue with SSO authenticator instantiating */
MSG_AUTHENTICATOR_ISSUE,
/** Error message format for not enabled remote launch config. */
MSG_REMOTE_LAUNCH_NOT_ENABLED,
/** Error message format for unauthorized user using config. */
MSG_UNAUTH_KEY_USE,
/** Error message format for no connection to the server default UI message. */
MSG_SDK_DEFAULT_REQUEST_ERROR
}
/**
* Listener for client sessions. Registers / removes from the in-memory collection all web sessions on
* their initialization / termination. Used by the One Browser Policy.
*/
private static class WebSessionListener
implements InitTermListener
{
/**
* This method is invoked when business logic main entry point is about to be invoked.
*/
@Override
public void initialize()
{
if (!StandardServer.getClientParameters().web)
{
return;
}
String deviceId = SessionUtils.getDeviceId().toStringMessage();
String sessionId = SessionUtils.getSessionId().toStringMessage();
List<String> sessionIds;
if (deviceIdSessionIdPairs.containsKey(deviceId))
{
sessionIds = deviceIdSessionIdPairs.get(deviceId);
}
else
{
sessionIds = new ArrayList<>();
deviceIdSessionIdPairs.put(deviceId, sessionIds);
}
sessionIds.add(sessionId);
Object sessionNetId = SecurityManager.getInstance().sessionSm.getSessionId();
sessionIdNetIdPairs.put(sessionId, sessionNetId);
sessionIdClientPairs.put(sessionId, new WeakReference<>(LogicalTerminal.getClient()));
}
/**
* This method is invoked when business logic entry point finished its work.
*
* @param t
* Error or exception which caused exit from business logic. Can be <code>null</code>.
*/
@Override
public void terminate(Throwable t)
{
if (!StandardServer.getClientParameters().web)
{
return;
}
String deviceId = SessionUtils.getDeviceId().toStringMessage();
String sessionId = SessionUtils.getSessionId().toStringMessage();
if (deviceIdSessionIdPairs.containsKey(deviceId))
{
deviceIdSessionIdPairs.get(deviceId).remove(sessionId);
}
sessionIdNetIdPairs.remove(sessionId);
sessionIdClientPairs.remove(sessionId);
}
}
}