# 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]));
             }
          }
       }
    }