ProcessClientSpawner.java

/*
** Module   : ProcessClientSpawner.java
** Abstract : define the process-based client spawner
**
** Copyright (c) 2014-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description-----------------------------------
** 001 CA  20140206 First version.
** 002 MAG 20140224 Use ProcessBuilderParameters for processes.
** 003 MAG 20140310 On Windows OS we must use also a password.
** 004 OM  20150108 Passed null map as additional environment to ProcessClientSpawner.spawn().
** 005 GES 20150505 Made the inner class private.
** 006 ECF 20150828 Enable Java 8.
** 007 GES 20160301 Added some javadoc.
** 008 SBI 20160628 Added new methods to allocate and to release the client's system resources.
** 009 SBI 20171019 Changed to allocate new system resources in order to run new web client on
**                  the target host.
** 010 CA  20191211 When spawning a Java process programmatically, allow the caller to pass 
**                  environment options specific to that call (beside any jvmArgs configured in 
**                  the directory).
** 011 SBI 20230411 Removed unused code.
** 012 GBB 20240709 Fixing typization for ProcessBuilderOptions, ProcessClientBuilder and TemporaryClientTask.
**                  Print diagnostics as part of TemporaryClient.doWork.
** 013 GBB 20240719 Logging cleanup.
** 014 GBB 20241010 Always pass true for security:certificate:validate.
** 015 GBB 20250106 Adding static inner class Params and contructor accepting param of type Params.
** 016 AP  20250310 Added parameter for default timeout.
*/
/*
** 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.util.*;
import java.util.function.*;

import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.directory.Base64;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.ui.client.chui.driver.web.*;
import com.goldencode.p2j.ui.client.driver.*;
import com.goldencode.p2j.util.*;

/**
 * Class used to spawn a new P2J client which will authenticate as a P2J process.
 * <p>
 * This class is tricky to understand because the main part of it runs on the server while the
 * <code>TemporaryClientTask</code> inner class is serialized and sent down to execute on the
 * client side.  That client side code then calls methods which are implemented as a remote
 * object with the {@link Spawner} interface, which the server portions of this class implement. 
 * <p>
 * The spawner process will connect to the server using a secure socket and temporary credentials
 * to establish a secure session.  If that works, it will lookup the credentials to use for
 * spawning the client based on a unique UUID for this spawning session.  Using those credentials
 * it will start the client and notify the server about success or failure.
 * <p>
 * This notification must not be treated as the client process authenticated successfully; it
 * needs to be interpreted as a signal that the temporary connection has finished successfully.
 */
public class ProcessClientSpawner
extends ClientSpawner
{
   /** Wrapper class for constructor params. */
   public static class Params
   {
      /** The P2J process ID. */
      private final String processName;

      /** The OS system user to spawn the client under. */
      private final String osUser;

      /** The OS system user password. */
      private final String osPass;

      /** Map of additional environment properties, in the <code>key=value</code> form. */
      private final Map<String, String> envMap;
      
      /** Default timeout for spawner launch, expressed in seconds. If this is {@code null} the fallback -
       *  {@code ClientSpawner.DEFAULT_TIMEOUT} is used. */ 
      private final Integer defaultTimeout;
      
      /**
       * Public constructor.
       * 
       * @param    processName
       *           The P2J process ID.
       * @param    osUser
       *           The OS system user to spawn the client under.
       * @param    osPass
       *           The OS system user password.
       * @param    envMap
       *           Map of additional environment properties, in the <code>key=value</code> form.
       * @param    defaultTimeout
       *           Default timeout for spawner launch, expressed in seconds.
       */
      public Params(String processName, String osUser, String osPass,
                    Map<String, String> envMap, Integer defaultTimeout)
      {
         this.processName = processName;
         this.osUser = osUser;
         this.osPass = osPass;
         this.envMap = envMap;
         this.defaultTimeout = defaultTimeout;
      }

      /**
       * Getter for {@link #processName}.
       * 
       * @return See above.
       */
      public String getProcessName()
      {
         return processName;
      }
      
      /**
       * Getter for {@link #osUser}.
       *
       * @return See above.
       */
      public String getOsUser()
      {
         return osUser;
      }

      /**
       * Getter for {@link #osPass}.
       *
       * @return See above.
       */
      public String getOsPass()
      {
         return osPass;
      }

      /**
       * Getter for {@link #envMap}.
       *
       * @return See above.
       */
      public Map<String, String> getEnvMap()
      {
         return envMap;
      }
      
      /**
       * Get the default timeout for spawner launch.
       *
       * @return   See above.
       */
      public Integer getDefaultTimeout()
      {
         return defaultTimeout;
      }
   }
   /** Default timeout for spawner launch, expressed in seconds */
   private final Integer defaultTimeout;
   
   /** The {@link ClientBuilder} used to spawn the P2J client process. */
   private final ProcessClientBuilder cb;

   /** The P2J process ID being spawned by this instance.*/
   private String process;
   
   /**
    * Create a new spawner for P2J processes.
    *
    * @param    params
    *           Wrapper object for constructor params.
    */
   public ProcessClientSpawner(Params params)
   {
      this(params.getProcessName(), params.getOsUser(), params.getOsPass(), params.getEnvMap(), 
           params.getDefaultTimeout());
   }
   
   /**
    * Create a new spawner for P2J processes.
    * 
    * @param    process
    *           The P2J process ID.
    * @param    osUser
    *           The OS system user to spawn the client under.
    * @param    osPass
    *           The OS system user password.
    * @param    env
    *           Map of additional environment properties, in the <code>key=value</code> form.
    * @param    defaultTimeout
    *           Default timeout for spawner launch, expressed in seconds.           
    */
   public ProcessClientSpawner(String process, String osUser, String osPass, Map<String, String> env,
                               Integer defaultTimeout)
   {
      ProcessBuilderParameters buildParams = new ProcessBuilderParameters(osUser,
                                                                          osPass,
                                                                          new ProcessBuilderOptions(process, env));
      this.cb = new ProcessClientBuilder(buildParams);
      this.process = process;
      this.defaultTimeout = defaultTimeout;
   }
   
   /**
    * Create a new spawner for P2J processes.
    *
    * @param    process
    *           The P2J process ID.
    * @param    osUser
    *           The OS system user to spawn the client under.
    * @param    osPass
    *           The OS system user password.
    * @param    env
    *           Map of additional environment properties, in the <code>key=value</code> form.
    */
   public ProcessClientSpawner(String process, String osUser, String osPass, Map<String, String> env)
   {
      this(process, osUser, osPass, env, null);
   }
   
   /**
    * Get {@link ProcessBuilderParameters} used to spawn P2J clients.
    *
    * @return   See above.
    */
   @Override
   public ProcessBuilderParameters getBuildParams()
   {
      return cb.getParams();
   }

   /**
    * Launch the specified batch process.
    * <p>
    * If the P2J process is linked with an appserver, it will delegate the launching to
    * {@link AppServerLauncher#launch(String)}.  Else, it will use the {@link ProcessClientSpawner 
    * spawner} infrastructure to start a P2J client.
    * 
    * @param    batchProcess
    *           The name of the batch process to be launched.
    */
   public static void launch(String batchProcess)
   {
      launch(batchProcess, null);
   }

   /**
    * Launch the specified batch process.
    * <p>
    * If the P2J process is linked with an appserver, it will delegate the launching to
    * {@link AppServerLauncher#launch(String)}.  Else, it will use the {@link ProcessClientSpawner 
    * spawner} infrastructure to start a P2J client.
    * 
    * @param    batchProcess
    *           The name of the batch process to be launched.
    * @param    env
    *           Map of additional environment properties, in the <code>key=value</code> form.
    */
   public static void launch(String batchProcess, Map<String, String> env)
   {
      SecurityManager sm = SecurityManager.getInstance();
      
      String appserver = sm.getAppserverForProcess(batchProcess);
      if (appserver != null)
      {
         // appservers are special, so treat them now
         AppServerLauncher.launch(appserver);
      }
      else
      {
         // normal process, launch it
         ProcessClientSpawner spawner = new ProcessClientSpawner(batchProcess,
                                                                 sm.getSystemUser(batchProcess),
                                                                 sm.getSystemPassword(batchProcess),
                                                                 env);
         
         // get the user name from the directory
         spawner.spawn();
      }
   }
   
   /**
    * Get a map containing the P2J process secure credentials: the serialized process keystore and
    * trust store and other details, like target aliases in the keystore and truststore. 
    * 
    * @return   Map of security configs key : value.
    */
   private Map<String, Object> getServerData()
   {
      // return a map with the process defaults
      Map<String, Object> defaults = new HashMap<>();
      
      ProcessBuilderOptions opts = getBuildParams().getDirOptions();
      
      if (opts.getProcessAlias() != null)
         defaults.put("security:keystore:processalias", opts.getProcessAlias());
      if (opts.getTruststoreAlias() != null)
         defaults.put("security:truststore:alias", opts.getTruststoreAlias());
      if (opts.getKeyentryPassword() != null)
         defaults.put("access:password:keystore", opts.getKeystorePassword());
      if (opts.getKeyentryPassword() != null)
         defaults.put("access:password:keyentry", opts.getKeyentryPassword());
      if (opts.getTruststorePassword() != null)
         defaults.put("access:password:truststore", opts.getTruststorePassword());
      if (opts.getProcessKeystore() != null)
         defaults.put("security:keystore:bytes", opts.getProcessKeystore());
      if (opts.getTruststore() != null)
         defaults.put("security:truststore:bytes", opts.getTruststore());
      
      defaults.put("security:certificate:validate", true);
      
      return defaults;
   }
   
   /**
    * Get the default timeout for spawner launch.
    *
    * @return   See above.
    */
   @Override
   public int getDefaultTimeout()
   {
      return this.defaultTimeout != null && this.defaultTimeout != 0 ? this.defaultTimeout :
         super.getDefaultTimeout();
   }
   
   /**
    * Get a {@link ProcessClientBuilder} used to spawn P2J clients for the target {@link #process}.
    *           
    * @return   See above.
    */
   @Override
   protected ProcessClientBuilder getBuilder()
   {
      return cb;
   }

   /**
    * Get a {@link TemporaryClient} worker which will do the work after authenticating on the P2J
    * server using the temporary credentials.
    *
    * @param    uuid
    *           The client identifier uuid.
    * @param    projectToken
    *           The project token to be set for the temporary session.
    *           
    * @return   See above.
    */
   @Override
   public TemporaryClient getTemporaryClient(String uuid, String projectToken)
   {
      return new TemporaryClientTask(uuid, getServerData());
   }
   
   /**
    * On P2J client side, it will do custom work after authenticating using the temporary 
    * credentials.
    * <p>
    * For process clients case, it will retrieve from the P2J server the secure credentials
    * and save them in the {@link BootstrapConfig configuration} used to start the P2J process.
    */
   private static class TemporaryClientTask
   implements TemporaryClient
   {
      /** the process key store and trust stores from the server */
      private final Map<String, Object> securityConfigs;
      
      /** The unique id for the spawned client running the task. */
      private final String uuid;
      
      /**
       * Constructor.
       *
       * @param    uuid
       *           The client identifier uuid.
       * @param    securityConfigs
       *           A map of the security configs needed for setting up the client.
       */
      TemporaryClientTask(String uuid, Map<String, Object> securityConfigs)
      {
         this.uuid = uuid;
         this.securityConfigs = securityConfigs;
      }
      
      /**
       * On P2J client side, it will do custom work after authenticating using the temporary 
       * credentials.
       * <p>
       * For web chui clients case, it will create a {@link ChuiWebDriver} and start the web 
       * embedded server which will serve this client's requests.
       * <p>
       * Any custom configuration already existing in the {@code config} parameter will not
       * be overwritten.
       * 
       * @param    spawner
       *           The spawner remote access interface. 
       * @param    config
       *           The configuration to use for setup of the client and for the server connection.
       * @param    diagnostics
       *           Details about the client process.
       *
       * @return   Always {@code null}.
       * 
       * @throws   RuntimeException
       *           If there are problems accessing the secure credentials.
       */
      @Override
      public ScreenDriver doWork(Spawner spawner, BootstrapConfig config, Supplier<String> diagnostics)
      {
         Map<String, Object> defaults = securityConfigs;

         String  alias       = (String)  defaults.get("security:keystore:processalias");
         String  tsAlias     = (String)  defaults.get("security:truststore:alias");
         String  ksPass      = (String)  defaults.get("access:password:keystore");
         String  kePass      = (String)  defaults.get("access:password:keyentry");
         String  tsPass      = (String)  defaults.get("access:password:truststore");
         byte[]  keystore    = (byte[])  defaults.get("security:keystore:bytes");
         byte[]  truststore  = (byte[])  defaults.get("security:truststore:bytes");

         try
         {
            // save them at the bootstrap config. any client-side config has priority
            if (alias != null &&
                config.getConfigItem("security", "keystore", "processalias") == null)
            {
               config.setConfigItem("security", "keystore", "processalias", alias);
            }
            
            if (tsAlias != null &&
                config.getConfigItem("security", "truststore", "alias") == null)
            {
               config.setConfigItem("security", "truststore", "alias", tsAlias);
            }
            
            if (ksPass != null && 
                config.getConfigItem("access", "password", "keystore") == null)
            {
               config.setConfigItem("access", "password", "keystore", ksPass);
            }
            
            if (kePass != null &&
                config.getConfigItem("access", "password", "keyentry") == null)
            {
               config.setConfigItem("access", "password", "keyentry", kePass);
            }
            
            if (tsPass != null &&
               config.getConfigItem("access", "password", "truststore") == null)
            {
               config.setConfigItem("access", "password", "truststore", tsPass);
            }
            
            config.setConfigItem("security", "certificate", "validate", Boolean.TRUE.toString());
            
            if (keystore != null &&
                config.getConfigItem("security", "keystore", "filename") == null)
            {
               config.setConfigItem("security", "keystore", "bytes",
                                    Base64.byteArrayToBase64(keystore));
            }

            if (truststore != null &&
               config.getConfigItem("security", "truststore", "filename") == null)
            {
               config.setConfigItem("security", "truststore", "bytes",
                                    Base64.byteArrayToBase64(truststore));
            }
            
            // invalidate the security transport...
            config.setConfigItem("security", "transport", "refresh", "true");

            ClientCore.outputDiagnostics(config, diagnostics);

            spawner.clientIsReady(uuid, null);
         }
         catch (ConfigurationException e)
         {
            throw new RuntimeException(e);
         }
         
         return null;
      }
   }

   @Override
   public void releaseClient()
   {
   }
}