NativeSecureConnection.java

/*
** Module   : NativeSecureConnection.java
** Abstract : Helper for connecting to a remote P2J server from native code, via JNI.
**
** Copyright (c) 2014-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description---------------------------------
** 001 CA  20140211 Created initial version.
** 002 CA  20140226 Write any errors to STDERR instead of STDOUT, so they end up on the server's
**                  log.
** 003 MAG 20140718 Implements remote batch clients. Substitute placeholders on remote launch.
**     IAS 20210325 Added NIO configuration via BootstrapConfig
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 GBB 20240729 Initialize CentralLogger in try-catch.
*/
/*
** 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 com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.logging.*;

/**
 * Helper class for connecting to a remote P2J server from native code, via JNI. Used by the
 * {@code spawn} tool when no password is specified and authentication needs to be done via P2J.
 */
public class NativeSecureConnection
{
   /**
    * Establish a secure connection to the target P2J server and retrieve the spawn command
    * registered with this request, via the specified UUID.
    * <p>
    * Authentication will be performed with the temporary credentials set in the environment. If
    * the secure connection is not possible, the received certificate can not be validated or 
    * if the authentication is not possible, it returns {@code null}.
    * 
    * @param    port
    *           The P2J server's secure port.
    * @param    host
    *           The P2J server's host.
    * @param    alias
    *           The certificate alias.
    * @param    uuid
    *           The UUID identifying this request.
    * @param    misc
    *           Misc. BootstrapConfig overrides           
    * @return   The spawn command or {@code null}.
    */
   public static String[] command(int port, String host, String alias, String uuid, String misc)
   {
      // establish a secure connection to the P2J server running on the specified port; use the
      // certificate targeted by the specified alias for authentication.
      Session sess = null;
      try
      {
         CentralLogger.setMode(CentralLogger.Mode.SPAWNER_JVM);
         CentralLogger log = CentralLogger.get(NativeSecureConnection.class);
         
         BootstrapConfig cfg = new BootstrapConfig();
         cfg.setServer(false);
         cfg.setConfigItem("net",      "connection",     "secure",       "true");
         cfg.setConfigItem("net",      "server",         "secure_port",  Integer.toString(port));
         cfg.setConfigItem("net",      "server",         "host",         host);
         cfg.setConfigItem("net",      "queue",          "conversation", "false");
         cfg.setConfigItem("security", "certificate",    "validate",     "true");
         cfg.setConfigItem("security", "truststore",     "filename",     "srv-certs.store");
         cfg.setConfigItem("security", "truststore",     "alias",        alias);
         cfg.setConfigItem("security", "authentication", "type",         "program");
         
         // Get user credentials from environment  
         String subject = System.getenv(TemporaryAccount.P2J_SUBJECT);
         String password = System.getenv(TemporaryAccount.P2J_PASSWORD);

         cfg.setConfigItem("access", "subject",  "id",   TemporaryAccount.fromHex(subject));
         cfg.setConfigItem("access", "password", "user", TemporaryAccount.fromHex(password));

         String[] overrides = misc.split(" ");
         for (String override : overrides)
         {
            String[] args = override.split(":");
            if (args.length != 3)
            {
               log.severe("Invalid override: " + override + ", ignored");
               continue;
            }
            String [] last = args[2].split("=");
            if (last.length != 2)
            {
               log.severe("Invalid override: " + override + ", ignored");
               continue;
            }
            cfg.setConfigItem(args[0], args[1], last[0], last[1]);
         }
         @SuppressWarnings("unused")
         SecurityManager secMgr = SecurityManager.createInstance(cfg);
         SessionManager sessMgr = SessionManagerFactory.createLeafNode(cfg);
         
         // establish a secure connection with the remote P2J server
         sess = sessMgr.connectDirect(cfg, null, null);
         RemoteSpawner remote =
            (RemoteSpawner) RemoteObject.obtainNetworkInstance(RemoteSpawner.class, sess);
            
         // get the command in use for this UUID
         String[] cmd = remote.getCommand(uuid);
         
         return remote(cmd);
      }
      catch (Throwable t)
      {
         final String msg = "Problem retrieving the spawn command, using these settings: " +
                            "port [%d] host [%s] alias [%s] UUID [%s]";
         CentralLogger.get(NativeSecureConnection.class)
                      .severe(String.format(msg, port, host, alias, uuid), t);
         
         // do not allow exceptions to propagate into native code.
         return null;
      }
      finally
      {
         if (sess != null)
         {
            sess.terminate();
         }
      }
   }
   
   /**
    * Prepare command substituting placeholder with broker values.
    * 
    * @param   args
    *          Command arguments read from server.
    *          
    * @return  Command arguments having placeholders replaced.
    */
   private static String[] remote(String[] args)
   {
      // Get environment variables
      Map<String, String> env = System.getenv();      
      // host
      String host = env.get(BrokerCore.BROKER_HOST);      
      // secure port
      String port = env.get(BrokerCore.BROKER_PORT);      
      // JVM arguments
      String jvmargs = env.get(BrokerCore.BROKER_JVMARGS);
      // classpath
      String classpath = env.get(BrokerCore.BROKER_CLASSPATH);

      // Command line arguments
      List<String> command = new LinkedList<String>();
      
      // Replace placeholders
      for (String cmd : args)
      {
         // replace host
         if (cmd.indexOf(BrokerCore.PARAM_HOST) > -1)
         {
            if (host == null)
            {
               throw new NullPointerException("Null value not allowed for host.");
            }
            // host
            command.add(cmd.replace(BrokerCore.PARAM_HOST, host));
         }
         // replace port
         else if (cmd.indexOf(BrokerCore.PARAM_PORT) > -1)
         {
            if (port == null)
            {
               throw new NullPointerException("Null value not allowed for secure port.");
            }
            // secure port
            command.add(cmd.replace(BrokerCore.PARAM_PORT, port));
         }
         else if (BrokerCore.PARAM_JVM.equals(cmd))
         {
            if (jvmargs == null)
            {
               throw new NullPointerException("Null value not allowed for JVM arguments.");
            }
            // add JVM arguments
            String[] tokens = jvmargs.split("\\" + BrokerCore.BROKER_JVMARGS_SEPARATOR);
            command.addAll(Arrays.asList(tokens));
         }
         else if (BrokerCore.PARAM_CP.equals(cmd))
         {
            if (classpath == null)
            {
               throw new NullPointerException("Null value not allowed for CLASSPATH.");
            }            
            // add CLASSAPTH
            command.add("-classpath");
            command.add(classpath);
         }
         else
         {
            command.add(cmd);
         }
      }
      
      return command.toArray(new String[0]);
   }
}