ClientBuilder.java
/*
** Module : ClientBuilder.java
** Abstract : build and start a platform process.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description------------------------------------
** 001 MAG 20131118 First version.
** 002 MAG 20140206 Send the referrer address via command line.
** 003 CA 20140206 Refactored to make it unaware of the type of the P2J client being spawned.
** 004 CA 20140224 Redirect STDERR of spawned process to the server STDERR.
** 005 MAG 20140310 Set password.
** 006 MAG 20140710 Implements remote launcher.
** 007 GES 20150211 Removed application arguments support.
** 008 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 009 GES 20160219 Moved code here from the process builder to share the native secure
** connection setup processing.
** 010 OM 20160905 Small optimizations.
** 011 SBI 20171019 Changed to spawn remote clients on dedicated hosts.
** 012 GES 20210205 Allow configFile to reference an absolute path as well as the relative path.
** IAS 20210325 Added NIO configuration via BootstrapConfig
** IAS 20210827 Added SSL package seqNo tracking configuration via BootstrapConfig
** 013 SBI 20230306 Added new remoteStart with new parameters following the implementation logic that
** allocates network ports before it spawns a remote client process.
** SBI 20230307 Changed to deliver a uuid of the spawned client to BrokerManager.spawn as a parameter.
** 014 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 015 RFB 20230418 Changed addConfigFile to no longer deep-check the existence of the config
** file. Rather, it should just return the configFile, since it won't exist on
** the server itself. Ref. #4938.
** 016 GBB 20230825 Adding sso:subject:id to client cmd.
** 017 GBB 20231113 Adding sso:storage:id to client cmd.
** 018 EVL 20240118 Adding optional logger for native spawn process.
** 019 GBB 20240709 Hard-coded config names replaced by ConfigItem constants. Letting concrete
** implementations to provide ClientBuilderParameters.
** 020 GBB 20240718 Moving base client builder configs to a field in the respective options class.
** 021 GBB 20240729 Adding -DenableSlf4jApi jvm arg to spawned clients.
** 022 CA 20240910 'net:socket:nio' is enabled by default only for Java 8.
** 023 CA 20250221 Added constants for the spawner modes.
** 024 AL2 20250523 'net:socket:nio' is disabled by default.
*/
/*
** 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.util.*;
import java.util.logging.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
/**
* This class is used to create and spawn a client process using a
* {@link ProcessBuilder} instance. A special small tools are designed for
* Linux / Windows OS in order to spawn the process on the OS user account.
* Both tools are written in C using specific OS API and are build as executable
* using ant native target. The source code and the make file for this tools are
* available in the /src/native process directory.
* The password is provided vis stdin redirection on both platforms.
* The command line parameters for the spawned process are constructed from default
* parameters stored in directory and some specific parameters stored inside a
* <code>ClientBuilderParameters</code> structure.
*/
public abstract class ClientBuilder
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(ClientBuilder.class.getName());
/** Pending spawn commands to be retrieved by remote clients. */
private static final Map<String, String[]> pendingCmds = new HashMap<>();
/** A collection of command line tokens */
protected final List<String> command = new LinkedList<>();
/** Indicate a remote or local spawn */
protected boolean remote = false;
/** The ProcessBuilder instance */
private ProcessBuilder pb = new ProcessBuilder();
/**
* Returns the {@link ClientBuilderParameters} used for building the client.
*
* @return See above.
*/
public abstract ClientBuilderParameters getParams();
/**
* Get the spawn command registered for this UUID.
*
* @param uuid
* The UUID.
*
* @return See above.
*/
public static String[] getCommand(String uuid)
{
synchronized (pendingCmds)
{
return pendingCmds.get(uuid);
}
}
/**
* Remove the spawn command registered for this UUID.
*
* @param uuid
* The UUID.
*/
public static void removeCommand(String uuid)
{
synchronized (pendingCmds)
{
pendingCmds.remove(uuid);
}
}
/**
* Add custom client options to the {@link #command}. Don't use for options critical for initial
* client configuration.
*/
protected abstract void addClientOptions();
/**
* Get the spawn arguments for a specific implementation.
*
* @return See above.
*/
protected abstract List<String> getSpawnArguments();
/**
* Spawn's new local Java process.
*
* @param env
* A map with environment variables for temporary account.
*
* @return Process object.
*
* @throws IOException
* If something goes wrong.
*/
public Process localStart(Map<String, String> env) throws IOException
{
// set local
remote = false;
pb.environment().putAll(env);
ClientBuilderParameters params = getParams();
// we start in the spawner's dir
File spawnerDir = new File(params.getDirOptions().getBaseOptions().getSpawner());
pb.directory(spawnerDir.getAbsoluteFile().getParentFile());
pb.command(prepareToLaunch());
Process shell = pb.start();
if (params.isPasswordAuthentication())
{
// write password
OutputStreamWriter os = new OutputStreamWriter(shell.getOutputStream());
os.write(params.getOsPass());
os.close();
}
return shell;
}
/**
* Spawn's new remote Java process.
*
* @param dedicatedUser
* The dedicated user associated with the host on which the spawned client
* will be run
* @param brokers
* List of brokers.
* @param env
* A map with environment variables for temporary account.
*
* @return Process object.
*/
public BrokerSpawnResult remoteStart(String dedicatedUser, Brokers brokers, Map<String, String> env)
{
return remoteStart(dedicatedUser, brokers, env, null, null);
}
/**
* Spawn's new remote Java process.
*
* @param dedicatedUser
* The dedicated user associated with the host on which the spawned client
* will be run
* @param brokers
* List of brokers.
* @param env
* A map with environment variables for temporary account.
* @param allocator
* The web client allocator
* @param requestParameters
* The given requests parameters
*
* @return Process object.
*/
public BrokerSpawnResult remoteStart(String dedicatedUser,
Brokers brokers,
Map<String, String> env,
WebClientAllocator allocator,
String[] requestParameters)
{
// set remote
remote = true;
// get spawn cmd
List<String> spawnCmd = prepareToLaunch();
BrokerSpawnParameters args = new BrokerSpawnParameters();
args.setEnvironment(env);
args.setCommand(spawnCmd);
ClientBuilderParameters params = getParams();
args.setPasswordAuthentication(params.isPasswordAuthentication());
if (args.isPasswordAuthentication())
{
args.setPassword(params.getOsPass());
}
return BrokerManager.spawn(dedicatedUser, brokers, args, allocator, requestParameters, params.getUuid());
}
/**
* Initialize the command line parameters for a native secure connection, which is the
* secure authentication method used for non-interactive clients and those interactive clients
* which cannot be launched using a password. These client processes call the {@code spawn}
* tool using the {@code spawn 0 <secure-port> <server-hostname> <server-alias> <uuid>}
* syntax.
*
* @param cmd
* The data structure to initialize with the parametes for the native secure
* connection.
*/
void initNativeSecureConnection(List<String> cmd)
{
// add the mode that makes this a native secure connection
cmd.add(ClientSpawner.MODE_AUTH_SERVER);
ClientBuilderParameters params = getParams();
// add host and secure port
if (remote)
{
// for remote spawn use placeholders
cmd.add(BrokerCore.PARAM_PORT);
cmd.add(BrokerCore.PARAM_HOST);
}
else
{
// add secure port
cmd.add(String.valueOf(params.getSecurePort()));
// add host; this is hard-coded to localhost because is expected that the in-process JVM
// will connect to the same P2J server which initiated the request (as this is the P2J
// server knowing the command to be executed).
cmd.add("localhost");
}
// add server alias
cmd.add(params.getServerAlias());
// add UUID
cmd.add(params.getUuid());
BootstrapConfig config = SessionManager.get().config();
boolean nio = config.getBoolean(ConfigItem.SERVER_SOCKET_NIO, false);
cmd.add(ConfigItem.SERVER_SOCKET_NIO.fullOption() + "=" + nio);
boolean trackSeqNo = config.getBoolean(ConfigItem.SERVER_SSL_TRACKSEQNO, false);
cmd.add(ConfigItem.SERVER_SSL_TRACKSEQNO.fullOption() + "=" + trackSeqNo);
}
/**
* Prepare command line arguments for spawned process before to start.
*
* @return A list with command line arguments.
*/
private List<String> prepareToLaunch()
{
ClientBuilderParameters params = getParams();
if (params.getSecurePort() == -1)
{
throw new RuntimeException("Clients can be spawned only if the server is running in secure mode!");
}
// build the final command (which will be directly started or delayed until after P2J authentication
buildCommand();
// launch the spawner
List<String> spawnCmd = new ArrayList<>();
BaseClientBuilderOptions dirOptions = params.getDirOptions().getBaseOptions();
spawnCmd.add(remote ? BrokerCore.PARAM_SPAWNER : dirOptions.getSpawner());
spawnCmd.addAll(getSpawnArguments());
// set up native logger for spawn process
if (dirOptions.isSpawnerDebugEnabled())
{
spawnCmd.add("-L");
}
if (LOG.isLoggable(Level.FINE))
{
StringBuilder sb = new StringBuilder("Launching a new P2J client using the command: ");
for (String s : spawnCmd)
{
sb.append(s).append(" ");
}
LOG.log(Level.FINE, sb.toString().trim());
}
return spawnCmd;
}
/**
* Add server properties.
*
* @param secure
* Indicating to setup a secure <code>true</code>
* or an insecure <code>false</code> connection.
*/
private void addServerOptions(boolean secure)
{
// add target server ports
BootstrapConfig serverConfig = SessionManager.get().config();
String host = remote ?
BrokerCore.PARAM_HOST :
serverConfig.getString(ConfigItem.SERVER_HOST, "localhost");
addToCommand(ConfigItem.SERVER_HOST, host);
boolean nio = serverConfig.getBoolean(ConfigItem.SERVER_SOCKET_NIO, false);
addToCommand(ConfigItem.SERVER_SOCKET_NIO, nio);
boolean trackSeqNo = serverConfig.getBoolean(ConfigItem.SERVER_SSL_TRACKSEQNO, false);
addToCommand(ConfigItem.SERVER_SSL_TRACKSEQNO, trackSeqNo);
if (secure)
{
addToCommand(ConfigItem.SECURE, true);
if (remote)
{
command.add(ConfigItem.SERVER_SECURE_PORT.fullOption() + "=" + BrokerCore.PARAM_PORT);
}
else
{
addConfigIfExisting(serverConfig, ConfigItem.SERVER_SECURE_PORT);
}
}
else if (!addConfigIfExisting(serverConfig, ConfigItem.SERVER_PORT))
{
addConfigIfExisting(serverConfig, ConfigItem.SERVER_INSECURE_PORT);
}
}
/**
* Get a key value form configuration file.
* If key exists add the value to builder command.
*
* @param config
* Configuration file.
* @param clientConfig
* The client config to be read and added.
*
* @return <code>true</code> if successfully added, <code>false</code> otherwise.
*/
private boolean addConfigIfExisting(BootstrapConfig config, ConfigItem<Integer> clientConfig)
{
Integer value = config.getInt(clientConfig, null);
if (value == null)
{
return false;
}
addToCommand(clientConfig, value);
return true;
}
/**
* Adds a config option name : value to the command.
*
* @param clientConfig
* The client config to be read and added.
* @param value
* The value to be added.
*/
private <T> void addToCommand(ConfigItem<T> clientConfig, T value)
{
command.add(clientConfig.fullOption() + "=" + value);
}
/**
* Add Java CLASSPATH and the main class. For remote clients the CLASSPATH and JVM arguments
* are inherited from broker process.
*/
private void addClasspath()
{
BaseClientBuilderOptions dirOptions = getParams().getDirOptions().getBaseOptions();
if (remote)
{
command.add(BrokerCore.PARAM_CP);
}
else
{
// add CLASSPATH
command.add("-classpath");
command.add(dirOptions.getClassPath());
}
// add main class
Class<?> mainClass = dirOptions.getMainClass();
command.add(mainClass.getName());
}
/**
* Add client configuration file if exists.
*/
private void addConfigFile()
{
String configFile = getParams().getDirOptions().getBaseOptions().getConfigFile();
if (configFile != null)
{
command.add(configFile);
}
}
/**
* Build command for spawned process.
*/
private void buildCommand()
{
ClientBuilderParameters params = getParams();
BaseClientBuilderOptions options = params.getDirOptions().getBaseOptions();
// build the final command (which will be directly started or delayed until after P2J
// authentication
String userDir = options.getWorkingDirectory();
// user name
command.add(params.getOsUser());
// password
if (!params.isPasswordAuthentication())
{
String password = params.getOsPass();
command.add(password == null ? "\"\"" : password);
}
// working directory
command.add(userDir);
// Java launcher executable
command.add(options.getCommand());
// JVM arguments
if (remote)
{
// placeholder, will be replaced by broker with CLASSPATH and JVM arguments
command.add(BrokerCore.PARAM_JVM);
}
else
{
// add JVM arguments
command.addAll(options.getJvmArguments());
// native libraries path
command.add("-Djava.library.path=" + options.getLibPath());
}
if (CentralLogger.WRITE_TO_SLF4J_API && !CentralLoggerServer.getConfigs().isServerSide())
{
command.add("-D" + CentralLogger.JVM_ARG_ENABLE_SLF4j_API + "=true");
}
// add CLASSPATH and main class
addClasspath();
// add client configuration file
addConfigFile();
// add server options
addServerOptions(options.isSecure());
// add other client options
addClientOptions();
command.add(ConfigItem.SPAWNER_UUID.fullOption() + "=" + params.getUuid());
// ensure there are no empty or null arguments. an empty argument will make the
// ClientDriver to assume that is a bootstrap config file name and fail on startup.
command.removeIf(s -> s == null || s.trim().length() == 0);
if (!params.isPasswordAuthentication())
{
synchronized (pendingCmds)
{
// save this for later use
pendingCmds.put(params.getUuid(), command.toArray(new String[0]));
}
}
}
}