/*
** Module   : spawn.c
** Abstract : a simple tool able to spawn a process on an existing user account.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description---------------------------------
** 001 MAG 20131127 First version.
** 002 MAG 20140205 Fix PTY (pseudo terminal) assignment bug.
** 003 CA  20140212 Added no password authentication: uses a secure P2J connection and temporary
**                  credentials to authenticate the request.
** 004 MAG 20140224 Redirect STDOUT to a file for batch clients.
** 005 MAG 20140310 A password argument has been added for agents. The password argument is
**                  required only on Windows OS. For Linux / Unix OS this argument is ignored.
** 006 ECF 20150907 Removed -XX:MaxPermSize=64m default option, which is no longer supported in
**                  Java 8.
** 007 EVL 20160217 Adding Solaris 10 u8 customization for ioctl.h to compile.
** 008 EVL 20160227 Adding missed getline() replacement.
** 009 EVL 20160323 Solaris version does not need the ioctl(fd, TIOCSCTTY,...) to be called.
** 010 GES 20160217 Clear the password buffer before freeing the pointer so that it doesn't stay
**                  in memory.
** 011 EVL 20200120 Added logic to handle valid passwordless user helping to avoid segmentation
**                  fault issue.
** 012 EVL 20200518 Fixed issue with handling extra debug option for application server spawning.
** 013 EVL 20210115 Adding more extra logging to FWD server.log for possible spawn failures.
**     EVL 20210119 More debug info to output.
**     IAS 20210325 Added addional parameters for command
**     EVL 20210519 Fix for segmentation fault in strcmp() library call.  Added new error codes to inform
**                  the caller about system errors.
** 014 GBB 20230131 outputToFile passed in as arg to create client process command.
**                  stdout redirect in Java.
** 015 GBB 20230512 The extra debug option removed. Logging levels attached to each message.
** 016 GBB 20230523 Cleanup of unused `j` var.
** 017 GBB 20230612 Logging the spawner chdir error.
** 018 GBB 20230623 Adding "started" print to stderr to notify the server logger to stop listening for logs.
** 019 RFB 20230418 Handle when a tilde is passed in as the working directory so it can be expanded
**                  appropriately. Ref. #4938.
**     RFB 20230421 Added check for existence of the workdir to allow for separation of a missing
**                  directory from a permissions issue. Ref. #4938.
**     RFB 20230425 Add some robustness to the string handling of added code per review.
** 020 GBB 20231213 Adding slf4j-impl.jar to JVM classpath.
** 021 EVL 20240117 Getting back the ability to redirect error stream to log inside native code.
** 022 CA  20240227 Added aspectjrt.jar to the spawner's classpath.
** 023 GBB 20240311 Started keyword removed. Spawner logger to stop listening based on spawn timeout.
** 024 GBB 20240729 Replace dependency slf4j-impl with slf4j-api in classpath.
** 025 CA  20250221 Added mode '2' to allow check of OS credentials (username/password).
** 026 EVL 20250625 Moved working with JVM to get spawn commands into childs process to completely release
**                  memory allocated with JVM start JNI command.
**     EVL 20250714 Improved shared memory clean up/release after using completed.
*/

/*
** 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.
*/

#define _GNU_SOURCE
#include <syslog.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <pwd.h>
#include <shadow.h>
#include <signal.h>
#include <grp.h>
//#define _XOPEN_SOURCE 600
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <jni.h>

#ifdef PAM
#include <security/pam_appl.h>
#define PAM_SERVICE_NAME "common-auth"
#else
#include <crypt.h>
#endif

#define P2J_JAR    "p2j.jar"
#define KEY_STORE  "srv-certs.store"

#define MIN_ARGS_CREDS       3
#define MIN_ARGS_PASSWORD    4
#define MIN_ARGS_NO_PASSWORD 6
#define MIN_ARGS_COMMAND     4
#define EXIT_SUCCESS         0

#define ERR_SPAWN_NO_ROOT_PERMISSION -1
#define ERR_SPAWN_NO_ARGS            -2
#define ERR_SPAWN_NO_PASSWORD        -3
#define ERR_SPAWN_POSIX_OPENPT       -4
#define ERR_SPAWN_GRANPT             -5
#define ERR_SPAWN_UNLOCKPT           -6
#define ERR_SPAWN_OPEN_SLAVE         -7
#define ERR_SPAWN_IOCTL              -8
#define ERR_SPAWN_SETGID             -9
#define ERR_SPAWN_SETEGID           -10
#define ERR_SPAWN_INITGROUPS        -11
#define ERR_SPAWN_SETUID            -12
#define ERR_SPAWN_SETEUID           -13
#define ERR_SPAWN_SETSID            -14
#define ERR_SPAWN_CHDIR             -15
#define ERR_SPAWN_SETPGID           -16
#define ERR_SPAWN_FORK_LEADER       -17
#define ERR_SPAWN_FORK_COMMAND      -18
#define ERR_SPAWN_CHDIR_WD          -19
#define ERR_SPAWN_STAT_WD           -20
#define ERR_SPAWN_SHMEM_GET         -21
#define ERR_SPAWN_SHMEM_AT          -22
#define ERR_SPAWN_SHMEM_DT          -23
#define ERR_SPAWN_SHMEM_FREE        -24
#define ERR_SPAWN_FORK_SHMEM        -25

#define ERR_JNI_JVM_CREATE             -100
#define ERR_JNI_P2J_ENTRY_POINT_CLASS  -101
#define ERR_JNI_P2J_ENTRY_POINT_METHOD -102
#define ERR_JNI_REMOTE_SERVER_PORT     -103
#define ERR_JNI_REMOTE_SERVER_HOST     -104
#define ERR_JNI_REMOTE_SERVER_ALIAS    -105
#define ERR_JNI_REMOTE_SERVER_UUID     -106
#define ERR_JNI_REMOTE_COMMAND         -107
#define ERR_JNI_JVM_DESTROY            -108
#define ERR_JNI_REMOTE_SERVER_MISC     -109

#define AUTH_OK                        0
#define AUTH_ERR_USER                  1
#define AUTH_ERR_PASSWORD              2
#define AUTH_ERR_ROOT                  3
#define AUTH_ERR_PAM                   4
#define AUTH_ERR_NULL_ACCOUNT_PASSWORD 5
#define AUTH_ERR_CRYPT                 6

#define DEBUG_JVM 0

#define MAX_FILENAME 4096
/* Shared memory size, just two 4k pages to have enogh room to keep filename and commands */
#define SHM_SIZE 8192
#define SHM_PERM_RW_R_NO 0640

#ifdef SOLARIS
   #define ALLPERMS (S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO)/* 07777 */
#endif

/* prototypes */
void stdout_redirect();

/* Global data */
struct passwd *pw;
char message_buffer[MAX_FILENAME];

/* File descriptors */
int fdm, fds;

/* Output file name */
char output_file_name[MAX_FILENAME];
int output_to_file_opt_found = 0;
/* Native log file name */
char log_file_name[MAX_FILENAME];
int native_log_found = 0;

/**
 * Use syslog to audit the execution of child processes.
 *
 * @param    code
 *           Exit code.
 * @param    method
 *           Method name where error is throw.
 *           
 * @return   Exit code
 */
int audit(int code, const char *method)
{
   fprintf(stderr, "SEVERE: %d method:%s \n", code, method);
   openlog("spawn", LOG_PID, LOG_USER);
   syslog(LOG_ERR, "Error:%d method:%s (%m)\n", code, method);
   closelog();
   return code;
}

/**
 * Writes message to stderr if extra logging is required.
 *
 * @param    msg
 *           The message to log.
 */
void logMessage(const char *msg)
{
    fprintf(stderr, "INFO: %s\n", msg);
}

/**
 * Assign a controlling terminal to a process.
 *
 * @return   0 - Success
 *           1 - Error
 */
int assign_pty()
{
   int result;

   /* Open a pseudo-terminal device */
   fdm = posix_openpt(O_RDWR);
   if (fdm < 0)
   {
      return audit(ERR_SPAWN_POSIX_OPENPT, "posix_openpt");
   }

   /* Grant access to the slave pseudo-terminal device */
   result = grantpt(fdm);
   if (result != 0)
   {
      return audit(ERR_SPAWN_GRANPT, "grantpt");
   }

   /*  Unlock a pseudo-terminal master/slave pair */
   result = unlockpt(fdm);
   if (result != 0)
   {
      return audit(ERR_SPAWN_UNLOCKPT, "unlockpt");
   }

   /* open slave pseudo-terminal device */
   fds = open(ptsname(fdm), O_RDWR);
   if (fds < 0)
   {
      return audit(ERR_SPAWN_OPEN_SLAVE, "open(ptsname)");
   }

#ifndef SOLARIS
   /* Use the slave as TTY */
   if (ioctl(fds, TIOCSCTTY, (char *)1) == -1)
   {
      return audit(ERR_SPAWN_IOCTL, "ioctl");
   }
#endif

   return EXIT_SUCCESS;
}

/**
 * Expand the tilde in the given path.
 * This should only be called if the first character of the given path
 * is a tilde. It will expand that to the environment's HOME directory.
 *
 * @param    path
 *           Path starting with tilde to expand.
 * @param    home
 *           The home directory.
 */
void expand_tilde(char *path, char *home)
{
   size_t homedir_len;
   char *homedir = (home != NULL) ? home : getenv("HOME");
   homedir_len = strlen(homedir);

   /*
    * Relocate the existing path information (starting with the character after the tilde) to the
    * same buffer, but leave space for the homedir to be positioned with the memcpy().
    * The existing null-terminator will also be relocated to the end of the path buffer.
    */
   memmove(path + homedir_len, path + 1, strlen(path));
   memcpy(path, homedir, homedir_len);

   return;
}

/**
 * Execute a command on a specified user account.
 * A child process is created via fork().
 * The child process uid and gid are set as the new user uid and gid.
 * Create a new session for child. The child became a session leader.
 * Assign a controlling terminal to session leader.
 * The child process environment is set from the new user account.
 * Create a child process to execute the command.
 *
 * @param    command
 *           Command to execute
 * @param    argv
 *           Command arguments.
 * @param    workdir
 *           Process working directory.
 *           
 * @return   Process exit status.
 */
int spawn(const char* command, char* const argv[], const char* workdir)
{
   pid_t child_pid, proc_pid;
   int status, result;

   /* Creates a child process */
   child_pid = fork();

   if (child_pid == 0)
   {
      /* Set gid */
      if (setgid(pw->pw_gid) < 0  || getgid() != pw->pw_gid)
      {
         sprintf(message_buffer, "Unable to set GID for %d.", pw->pw_gid);
         logMessage(message_buffer);
         exit(audit(ERR_SPAWN_SETGID, "setgid"));
      }

      /* Set egit*/
      if (setegid(pw->pw_gid) < 0 || getegid() != pw->pw_gid)
      {
         sprintf(message_buffer, "Unable to set EGID for %d.", pw->pw_gid);
         logMessage(message_buffer);
         exit(audit(ERR_SPAWN_SETEGID, "setegid"));
      }

      /* Initialize the supplementary group access list */
      if (initgroups(pw->pw_name, pw->pw_gid) < 0)
      {
         sprintf(message_buffer, "Unable to init groups for PW_NAME %s and GID %d.", pw->pw_name, pw->pw_gid);
         logMessage(message_buffer);
         exit(audit(ERR_SPAWN_INITGROUPS, "initgroups"));
      }

      /* Set uid */
      if (setuid(pw->pw_uid) < 0 || getuid() != pw->pw_uid)
      {
         sprintf(message_buffer, "Unable to set UID for %d.", pw->pw_uid);
         logMessage(message_buffer);
         exit(audit(ERR_SPAWN_SETUID, "setuid"));
      }

      /* Set euid */
      if (seteuid(pw->pw_uid) < 0 || geteuid() != pw->pw_uid)
      {
         sprintf(message_buffer, "Unable to set EUID for %d.", pw->pw_uid);
         logMessage(message_buffer);
         exit(audit(ERR_SPAWN_SETEUID, "seteuid"));
      }

      /* Creates a session and sets the process group ID */
      if(setsid() < 0)
      {
         logMessage("Unable to set SID.");
         exit(audit(ERR_SPAWN_SETSID, "setsid"));
      }

      /* Assign controlling terminal */
      result = assign_pty();
      if (result != EXIT_SUCCESS)
      {
         logMessage("Unable to assign PTY.");
         exit(audit(result, "assign_pty"));
      }

      /* Set user environment */
      char* path = strdup(getenv("PATH"));
      setenv("HOME",pw->pw_dir,1);
      setenv("USER",pw->pw_name,1);
      setenv("LOGNAME",pw->pw_name,1);
      setenv("LOGIN",pw->pw_name,1); /*< Historical; only strictly needed on AIX */
      setenv("SHELL",pw->pw_shell[0] ? pw->pw_shell : "/bin/sh", 1);
      setenv("PATH",path,1);
      free(path);

      /* Change the current working directory */
      char* chHomeDir = getenv("HOME");
      if (chdir(chHomeDir) < 0)
      {
         if (chdir("/") < 0)
         {
            sprintf(message_buffer, "Unable to enter the home directory - %s.", chHomeDir);
            logMessage(message_buffer);
            exit(audit(ERR_SPAWN_CHDIR, "chdir($HOME)"));
         }
      }

      /* Spawn a child process */
      proc_pid = fork();

      if (proc_pid == 0)
      {
         /* Close master pty */
         close(fdm);

         /* 
         * Redirect STDIN and STDOUT to PTY. 
         * STDERR is always inherited.
         */
         dup2(fds, STDIN_FILENO); // PTY becomes standard input (0)
         dup2(fds, STDOUT_FILENO); // PTY becomes standard output (1)
      
         /* Cancel certain signals */
         signal(SIGCHLD,SIG_DFL); /* A child process dies */
         signal(SIGTSTP,SIG_IGN); /* Various TTY signals */
         signal(SIGTTOU,SIG_IGN);
         signal(SIGTTIN,SIG_IGN);
         signal(SIGHUP, SIG_IGN); /* Ignore hangup signal */
         signal(SIGTERM,SIG_DFL); /* Die on SIGTERM */

         /* Set process group ID */
         if (setpgid(0, child_pid) < 0)
         {
            sprintf(message_buffer, "Forked. Unable to set PGID for %d.", child_pid);
            logMessage(message_buffer);
            exit(audit(ERR_SPAWN_SETPGID, "setpgid"));
         }

         /* Set process working directory */
         char *expanded_workdir;
         size_t expanded_workdir_len = strlen(workdir) + strlen(pw->pw_dir) + 1;
         expanded_workdir = malloc(sizeof(char) * expanded_workdir_len);
         strcpy(expanded_workdir, workdir);

         if (expanded_workdir[0] == '~' && expanded_workdir[1] == '/')
         {            
            expand_tilde(expanded_workdir, pw->pw_dir);
         }

         struct stat sb;
         if (stat(expanded_workdir, &sb) == 0 && S_ISDIR(sb.st_mode))
         {
            snprintf(message_buffer, sizeof(message_buffer), "Forked. Setting working directory to %s.",
                     expanded_workdir);
            logMessage(message_buffer);
            if (chdir(expanded_workdir))
            {
               snprintf(message_buffer, sizeof(message_buffer),
                       "Forked. Unable to set working directory to %s. Error: %s, errno is %d",
                       expanded_workdir,
                       strerror(errno),
                       errno);
               logMessage(message_buffer);
               free(expanded_workdir);
               exit(audit(ERR_SPAWN_CHDIR_WD, "chdir(expanded_workdir)"));
            }
         }
         else
         {
            snprintf(message_buffer, sizeof(message_buffer), "Forked. Working directory (%s) does not exist.",
                     expanded_workdir);
            logMessage(message_buffer);
            free(expanded_workdir);
            exit(audit(ERR_SPAWN_STAT_WD, "stat(expanded_workdir)"));
         }
         free(expanded_workdir);
         
         /* Redirect STDOUT if required */
         if (native_log_found == 1)
         {
            stdout_redirect();
         }
         
         /* Execute command */
         exit(execvp(command, argv));
      }
      else if (proc_pid < 0)
      {
         /* On fork() fails */
         logMessage("Failure in fork() call.");
         exit(audit(ERR_SPAWN_FORK_COMMAND, "fork"));
      }

      /* Close slave pty */
      close(fds);

      /* Wait for child to exit*/
      wait(&status);

      /* Exit */
      exit(status);
   }
   else if (child_pid < 0)
   {
      /* On fork() fails */
      logMessage("Failure in child process creation.");
      exit(audit(ERR_SPAWN_FORK_LEADER, "fork"));
   }

   return EXIT_SUCCESS;
}

#ifdef PAM
struct pam_response *reply;

/**
 * PAM conversation function.
 * This is a minimal implementation.
 *
 * @param    num_msg
 *           The number of messages that are being passed to the function.
 * @param    msg
 *           A pointer to the buffer that holds the messages from the user.
 * @param    resp
 *           A pointer to the buffer that holds the responses to the user.
 * @param    appdata_ptr
 *           Pointer to the application data.
 *           
 * @return   PAM_SUCCESS      - success.
 */
int function_conversation(int num_msg,
                          const struct pam_message **msg,
                          struct pam_response **resp,
                          void *appdata_ptr)
{
   *resp = reply;
   return PAM_SUCCESS;
}

/**
 * PAM authentication.
 *
 * @param    username
 *           User name.
 * @param    password
 *           Use password.
 *           
 * @return   AUTH_OK         - success.
 *           AUTH_ERR_USER    - user does not exists.
 *           AUTH_ERR_PAM     - on other errors.
 */
int check_user(const char *username, char *password)
{
   int retval;
   const char *user;
   pam_handle_t *local_auth_handle = NULL; // this gets set by pam_start
   const struct pam_conv local_conversation = { function_conversation, NULL };

   /* The pam_start function creates the PAM context and initiates the PAM transaction. */
   retval = pam_start(PAM_SERVICE_NAME, username, &local_conversation, &local_auth_handle);

   if (retval != PAM_SUCCESS)
   {
      return audit(AUTH_ERR_PAM, "pam_start");
   }

   if (password != NULL)
   {
      reply = (struct pam_response *)malloc(sizeof(struct pam_response));
   
      reply->resp = password;
      reply->resp_retcode = 0;
   
      /* The pam_authenticate function is used to authenticate the user. */
      retval = pam_authenticate(local_auth_handle, 0);
   
      if (retval != PAM_SUCCESS)
      {
         switch (retval)
         {
            case PAM_USER_UNKNOWN:
               return audit(AUTH_ERR_USER, "pam_authenticate");
            default:
               return audit(AUTH_ERR_PAM, "pam_authenticate");
         }
      }
   }

   /* Get mapped user name; PAM may have changed it */
   retval = pam_get_item(local_auth_handle, PAM_USER, (const void **)&user);
   if (retval != PAM_SUCCESS)
   {
      return audit(AUTH_ERR_USER, "pam_get_item");
   }

   /* Get a pointer to a structure containing the broken-out fields
   of the record in the password database (e.g., the local password
   file /etc/passwd, NIS, and LDAP) that matches the username name. */
   pw = getpwnam(user);
   endpwent();

   if (!pw) return audit(AUTH_ERR_USER, "getpwnam"); //user doesn't really exist

   if (pw->pw_uid == 0)
   {
      return AUTH_ERR_ROOT;   // user is root
   }

   /* The pam_end function terminates the PAM transaction and is the
      last function an application should call in the PAM context. */
   retval = pam_end(local_auth_handle, retval);

   if (retval != PAM_SUCCESS)
   {
      return audit(AUTH_ERR_PAM, "pam_end");
   }

   return AUTH_OK;
}

#else

/**
 * Get user account informations including uid, gid and password.
 * If the user exists check for password match.
 * This is basically an authentication action.
 * The user root is not allowed.
 *
 * @param    username
 *           User name.
 * @param    password
 *           Use password.
 *           
 * @return   AUTH_OK         - success.
 *           AUTH_ERR_USER    - user does not exists.
 *           AUTH_ERR_PASSWORD - password does not match.
 *           AUTH_ERR_ROOT    - user is root.
 */
int check_user(const char* username, const char* password)
{
   struct spwd *sp;
   char *encrypted, *correct;
   int rc;

   sprintf(message_buffer, "Check user account for: %s.", username);
   logMessage(message_buffer);

   /* Get a pointer to a structure containing the broken-out fields
   of the record in the password database (e.g., the local password
   file /etc/passwd, NIS, and LDAP) that matches the username name. */
   pw = getpwnam(username);
   endpwent();

   if (!pw) return audit(AUTH_ERR_USER, "getpwnam"); //user doesn't really exist

   if (pw->pw_uid == 0)
   {
      return AUTH_ERR_ROOT;   // user is root
   }

   if (password != NULL)
   {
      logMessage("The password is not null.");
      /* Get a pointer to a structure containing the broken-out fields
      of the record in the shadow password database that matches the username name. */
      sp = getspnam(pw->pw_name);
      endspent();
   
      if (sp)
      {
         logMessage("GetSPNam not null, used password from structure.");
         correct = sp->sp_pwdp;
      }
      else
      {
         logMessage("GetSPNam is null, used password from passwd instead.");
         correct = pw->pw_passwd;
      }
      
      // here we separate passwordless user from missing password for regular user
      if (sp && strlen(password) == 0 && strlen(correct) == 1 && correct[0] == '!')
      {
         logMessage("Empty password login.");
         return AUTH_OK;
      }

      // internal error case
      if (correct == NULL)
      {
         sprintf(message_buffer,
                 "No password set for user account '%s', cannot compare against non-null input password.",
                 username);
         logMessage(message_buffer);
         return AUTH_ERR_NULL_ACCOUNT_PASSWORD;
      }

      encrypted = crypt(password, correct);

      // need to check to avoid NULL pointer in srtcmp()
      if (encrypted == NULL)
      {
         sprintf(message_buffer, "Password encryption error: %s, errno is %d.", strerror(errno), errno);
         logMessage(message_buffer);
         // code 22 - invalid argument
         if (errno == EINVAL)
         {
            sprintf(message_buffer, "Check the password setup for user '%s'.", username);
            logMessage(message_buffer);
         }
         return AUTH_ERR_CRYPT;
      }
   
      logMessage("Attempt to use login with password.");
      // bad pw=2, success=0
      rc = strcmp(encrypted, correct);

      sprintf(message_buffer, "Passwords comparison result: %d.", rc);
      logMessage(message_buffer);

      return rc ? AUTH_ERR_PASSWORD : AUTH_OK;
   }
   else
   {
      logMessage("The password is null.");
      return AUTH_OK;
   }
}

#endif

/**
 * Check executable file attributes.
 *
 * @param    path
 *           path to executable file.
 * @param    permissions
 *           The file's permissions
 *           
 * @return   0 - OK.
 *           1 - No root permissions.
 */
int check_root(const char *path, int permissions)
{
   struct stat pstat;
   int result;

   result = stat(path, &pstat);

   if (!result)
   {
      result = (pstat.st_uid == 0) &&
               (pstat.st_gid == 0) &&
               (pstat.st_mode & ALLPERMS) == permissions ? 0 : 1;
   }
   else
   {
      audit(result, "stat");
   }

   return result;
}

#ifdef SOLARIS
#define _BUFSIZE_GET_LINE 1024
#define getline get_line

/**
* Get line of characters from the stream.
*
* @param   lineptr
*          pointer to the buffer to store the line.
* @param   line_size
*          the size of the line buffer.
* @param   stream
*          the stream to read the data from.
*
* @return  The number of chars read from the stream or -1 if EOF encountered before any chars.
*/
ssize_t get_line(char **line_ptr, size_t *line_size, FILE *stream)
{
   // the internal line buffer
   char * buff;
   // bytes in currently allocated buffer
   size_t bytes_allocated = 0;
   // current character
   int curr_char;
   // the length of the chars already being read
   size_t llength = 0;
   
   // check if we need to allocate memory for line buffer
   if (*line_ptr == NULL)
   {
      // need to allocate new buffer
      bytes_allocated = _BUFSIZE_GET_LINE;
      buff = malloc(sizeof(char) * bytes_allocated);
   }
   else
   {
      // buffer already exists
      buff = *line_ptr;
      bytes_allocated = *line_size;
   }
   
   // read buffer until new line char or EOF
   do
   {
      // get the next character
      curr_char = fgetc(stream);

      // end of file - do nothing for char storing
      if (curr_char != EOF)
      {
         // no moe space in a buffer - need to reallocate
         if (llength >= bytes_allocated)
         {
            // new size is old plus buffer size
            bytes_allocated += _BUFSIZE_GET_LINE;
            buff = realloc(buff, sizeof(char) * bytes_allocated);
         }
         
         // store the next char
         *(buff + llength) = (unsigned char)curr_char; 
         llength++;
      }
      
   } while (curr_char != '\n' && curr_char != EOF);
   
   // check the results
   if (llength == 0)
   {
      // buffer was allocated here, need to free memory
      if (buff != NULL && *line_ptr == NULL)
      {
         free(buff);
         buff = NULL;
      }
      // result must be -1 meaning error
      llength = -1;
      // setting the error value to I/O error
      errno = EIO;
   }
   else
   {
      // buffer is not empty, set up the NULL terminated string
      if (buff != NULL)
      {
         buff[llength] = '\0';
         // return the buffer too including possible new line character
         *line_ptr = buff;
      }
      else
      {
         // this is pretty impossible case, meeans something is wrong, return -1 meaning error
         llength = -1;
         // let's consider we have no memory to allocate
         errno = ENOMEM;
      }
   }
   // store the bytes required to allocate
   *line_size = bytes_allocated;
   
   return llength;
}
#endif

/**
 * Overwrite the given buffer to erase the contained data and free the pointer.
 *
 * @param    buf
 *           The buffer to zap.
 * @param    len
 *           The length of the buffer.
 */
void zap(char** buf, size_t* len)
{
   if (*buf != NULL)
   {
      int i;
      
      int   z   = *len;
      char* ptr = *buf;
      
      for (i = 0; i < z; i++)
      {
         *(ptr + i) = 0;
      }
      
      free(ptr);
      *buf = NULL;
   }
   
   /* make sure our caller doesn't get confused */
   *len = 0;
}

/**
 * Get the user password from stdin.
 *
 * @param    len
 *           On output, this will be set to the length of the returned buffer.
 *
 * @return   The read password or NULL on error.
 */
char* password(size_t* len)
{
   /* by setting these to NULL and 0, getline() will malloc() a buffer */
   char* pswd = NULL;
   *len = 0;

   /* read a line from the console */
   int read = getline(&pswd, len, stdin);
   
   sprintf(message_buffer, "The password length(data read): %lu(%d).", *len, read);
   logMessage(message_buffer);

   if (read == -1)
   {
      sprintf(message_buffer, "The password read error: %d.", errno);
      logMessage(message_buffer);
      if (errno == 0)
      {
         // considering EOF encountered 
         if (pswd == NULL)
         {
            pswd = malloc(1);
         }
         else
         {
            pswd = realloc(pswd, 1);
         }
         pswd[0] = '\0';
      }
      /* error has occurred, not sure if this can happen but we are being safe */
      else if (pswd != NULL)
      {
         zap(&pswd, len);
      }
   }
   else if (read == 0)
   {
      if (pswd == NULL)
      {
         pswd = malloc(1);
      }
      else
      {
         pswd = realloc(pswd, 1);
      }
      pswd[0] = '\0';
   }
   else if (pswd[read - 1] == '\n')
   {
      /* remove the newline if it is there */
      pswd[read - 1] = 0;
   }

   return pswd;
}

/**
 * Print help.
 *
 * @param    pname
 *           The program's name.
 */
int help(char *pname)
{
   printf("Usage:\n");
   printf("- for password authentication: %s 1 <user> <workdir> <command> [args]\n", pname);
   printf("- for server authentication: %s 0 <secure-port> <host> <server-alias> <uuid>\n", pname);
   printf("- for user credentials check: %s 2 <user>\n", pname);
   
   return EXIT_SUCCESS;
}

/**
 * Launch an in-process JVM and connects to the specified server. Authentication is assumed passed
 * only if a secure connection can be establiashed with the server and the server's certificate  
 * matches one from the store.
 * <p>         
 * If authentication passes, starts a new process with the real command.
 *
 * @param    cport
 *           The P2J server's port.
 * @param    chost
 *           The P2J server's host.
 * @param    calias
 *           The certificate alias for the P2J server.
 * @param    uuid
 *           Unique identifier for this request; must be known by the target P2J server.
 */
int launchP2JClient(char cport[], char chost[], char calias[], char cuuid[], char cmisc[])
{
   /* variables to use shared memory in child process to work with JVM */
   pid_t child_pid;
   int status = 0;
   int shmid;

   /* prepare shared memory to use inside child process, permissions: rw-r----- */
   shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_PERM_RW_R_NO | IPC_CREAT);
   if (shmid < 0)
   {
      /* On shmget() fails */
      logMessage("Failure in shared memory allocation request.");
      exit(audit(ERR_SPAWN_SHMEM_GET, "shmget"));
   }
   
   /* starting child process to work with JVM */
   child_pid = fork();
   if (child_pid == 0)
   {
      // get shared memory region from starting with full access
      void * sh_data = shmat(shmid, (void *)0, 0);
      if (sh_data == (void *)-1)
      {
         exit(audit(ERR_SPAWN_SHMEM_AT, "shmat in child"));
      }
      /* alias to original address */
      void * sh_data_orig = sh_data;

      /* inside child process */
      JavaVM *jvm;
      JNIEnv *env;

      int nopts;
   
      JavaVMOption options[8];
      options[0].optionString = "-Djava.class.path=./p2j.jar:./slf4j-api.jar:./aspectjrt.jar:.";
      options[1].optionString = "-Djava.lib.path=.";
      options[2].optionString = "-Xmx512m";
      options[3].optionString = "-Djava.awt.headless=true";
      options[4].optionString = "-Djava.compiler=NONE";
      nopts = 5;
   
      if (DEBUG_JVM)
      {
         options[5].optionString = "-Xdebug";
         options[6].optionString = "-Xnoagent";
         options[7].optionString = "-Xrunjdwp:transport=dt_socket,address=2999,server=y,suspend=y";
         nopts = 8;
      }

      JavaVMInitArgs vm_args; /* VM initialization arguments */
      vm_args.version   = JNI_VERSION_1_6;
      vm_args.nOptions  = nopts;
      vm_args.options   = options;
      vm_args.ignoreUnrecognized = 0;

      jint err = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
      if (err != JNI_OK)
      {
         exit(audit(ERR_JNI_JVM_CREATE, "JNI_CreateJavaVM"));
      }

      jclass cls = (*env)->FindClass(env, "com/goldencode/p2j/main/NativeSecureConnection");
      if (cls == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);

         exit(audit(ERR_JNI_P2J_ENTRY_POINT_CLASS, "FindClass(NativeSecureConnection)"));
      }
   
      char *signature =
            "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;";
      jmethodID mid = (*env)->GetStaticMethodID(env, cls, "command", signature);
      if (mid == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);

         exit(audit(ERR_JNI_P2J_ENTRY_POINT_METHOD, "GetStaticMethodID(command)"));
      }
   
      int port = atoi(cport);
   
      jstring host = (*env)->NewStringUTF(env, chost);
      if (host == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);

         exit(audit(ERR_JNI_REMOTE_SERVER_HOST, "NewStringUTF(host)"));
      }

      jstring alias = (*env)->NewStringUTF(env, calias);
      if (alias == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);
      
         exit(audit(ERR_JNI_REMOTE_SERVER_ALIAS, "NewStringUTF(alias)"));
      }

      jstring uuid = (*env)->NewStringUTF(env, cuuid);
      if (uuid == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);

         exit(audit(ERR_JNI_REMOTE_SERVER_UUID, "NewStringUTF(uuid)"));
      }

      jstring misc = (*env)->NewStringUTF(env, cmisc);
      if (misc == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);

         exit(audit(ERR_JNI_REMOTE_SERVER_MISC, "NewStringUTF(misc)"));
      }

      jobjectArray command = (*env)->CallStaticObjectMethod(env, (jclass) cls, (jmethodID) mid,
                                                            port, host, alias, uuid, misc);
   
      if (command == NULL)
      {
         (*jvm)->DestroyJavaVM(jvm);
      
         exit(audit(ERR_JNI_REMOTE_COMMAND, "CallStaticObjectMethod(command)"));
      }
   
      /* extract the arguments */
      jsize jlength = (*env)->GetArrayLength(env, (jarray) command);
      /* store arguments length */
      memcpy(sh_data, &jlength, sizeof(jsize));
      /* advance pointer to next memory address to write */
      sh_data += sizeof(jsize);

      /* store all arguments coming from JVM in shared memory */
      int i;
      for (i = 0; i < (int) jlength; i++)
      {
         jstring jarg = (jstring) (*env)->GetObjectArrayElement(env, command, (jsize) i);
         jsize argLength = (*env)->GetStringUTFLength(env, jarg);
         const char *narg = (*env)->GetStringUTFChars(env, jarg, 0);

         if (narg != NULL)
         {
            sprintf(message_buffer, "The argument number %d is: %s.", i, narg);
            logMessage(message_buffer);

            int nlength = (int) argLength + 1;
            /* argument length does not include terminated null char */
            memcpy(sh_data, &argLength, sizeof(jsize));
            /* advance pointer to next memory address to write */
            sh_data += sizeof(jsize);
            memset(sh_data, '\0', nlength);
            strcpy(sh_data, narg);
            /* advance pointer to next memory address to write */
            sh_data += nlength;
         }

         (*env)->ReleaseStringUTFChars(env, jarg, narg);
      }

      /* detach shared memory*/
      if (shmdt(sh_data_orig) == -1)
      {  
         exit(audit(ERR_SPAWN_SHMEM_DT, "shmdt in child"));
      }

      err = (*jvm)->DestroyJavaVM(jvm);
      if (err < 0)
      {
         exit(audit(ERR_JNI_JVM_DESTROY, "DestroyJavaVM"));
      }
      
      /* terminate child process */
      exit(EXIT_SUCCESS);
   }
   else if (child_pid < 0)
   {
      /* On fork() fails */
      logMessage("Failure in child process creation to process JVM.");
      exit(audit(ERR_SPAWN_FORK_SHMEM, "fork for JVM"));
   }
      
   /* Wait for child to exit, no need to semaphore usage */
   wait(&status);
      
   /* process shared memory results */
      
   /* get shared memory region */
   void * sh_data = shmat(shmid, (void *)0, SHM_RDONLY);
   if (sh_data == (void *)-1)
   {
      exit(audit(ERR_SPAWN_SHMEM_AT, "shmat in parent"));
   }
   /* alias to original address */
   void * sh_data_orig = sh_data;
      
   int length = 0;
   memcpy(&length, sh_data, sizeof(jsize));
   sh_data += sizeof(jsize);
      
   /* make it 1-based, so it follows the structure of the command line arguments 
      (0 is program name); also, one more position is needed for execvp, which requires for the
      arguments to be null-terminated. */
   char **arguments = (char**) malloc((length + 2 + output_to_file_opt_found) * sizeof(char*));
   arguments[0] = NULL;
   arguments[length + 1 + output_to_file_opt_found] = NULL;

   if (output_to_file_opt_found == 1)
   {
      int size = (strlen(output_file_name) + 1) * sizeof(char);
      arguments[length + 1] = (char*) malloc(size);
      memset(arguments[length + 1], '\0', size);
      strcpy(arguments[length + 1], output_file_name);
   }
      
   /* extract argumants from shared memory*/
   int j;
   for (j = 0; j < length; j++)
   {
      int argLen = 0;

      /* argument length does not include terminated null char */
      memcpy(&argLen, sh_data, sizeof(jsize));
      sh_data += sizeof(jsize);
         
      int nlen = (int) argLen + 1;
      arguments[j + 1] = (char*) malloc(nlen * sizeof(char));
      memset(arguments[j + 1], '\0', nlen);
      strcpy(arguments[j + 1], sh_data);
      sh_data += nlen;         
   }
      
   /* detach shared memory*/
   if (shmdt(sh_data_orig) == -1)
   {
      exit(audit(ERR_SPAWN_SHMEM_DT, "shmdt in parent"));
   }
   
   /* release OS resources */
   if (shmctl(shmid, IPC_RMID, NULL) == -1)
   {
      exit(audit(ERR_SPAWN_SHMEM_FREE, "shmctl in parent to release shared memory"));      
   }
   
   if (length < MIN_ARGS_COMMAND)
   {
      help("");
      exit(ERR_SPAWN_NO_ARGS);
   }
   else
   {
      /* Check user account credentials */
      int result = check_user(arguments[1], NULL);
   
      switch (result)
      {
         case AUTH_OK:
            sprintf(message_buffer,
                    "Starting with argv[MIN_ARGS_COMMAND]: (%s), argv[MIN_ARGS_COMMAND - 1]: (%s).",
                    arguments[MIN_ARGS_COMMAND],
                    arguments[MIN_ARGS_COMMAND - 1]);
            logMessage(message_buffer);
            result = spawn(arguments[MIN_ARGS_COMMAND],
                           &arguments[MIN_ARGS_COMMAND],
                           arguments[MIN_ARGS_COMMAND - 1]);
            break;
         case AUTH_ERR_USER:
            fputs("SEVERE: Invalid user name.\n", stderr);
            break;
         case AUTH_ERR_PASSWORD:
            fputs("SEVERE: Invalid password.\n", stderr);
            break;
         case AUTH_ERR_ROOT:
            fputs("SEVERE: User root not allowed.\n", stderr);
            break;
         case AUTH_ERR_NULL_ACCOUNT_PASSWORD:
            fputs("SEVERE: No password set for user account.\n", stderr);
            break;
         case AUTH_ERR_CRYPT:
            fputs("SEVERE: Password encryption error.\n", stderr);
            break;
      }
      sprintf(message_buffer, "Spawn result code: (%d).", result);
      logMessage(message_buffer);
      return result;
   }

   return EXIT_SUCCESS;
}

/**
 * Replace a placeholder within a string.
 *
 * @param    string
 *           Original string.
 * @param    placeholder
 *           Placeholder string.
 * @param    value
 *           Replacement string value.
 *           
 * @return   The string after placeholder was replaced.
 *           If the placeholder is not found returns original string.
 */
char* replace_str(char* string, const char* placeholder, char* value)
{
   static char buffer[MAX_FILENAME];
   int len;
   char *p;

   p = strstr(string, placeholder);
   
   if (!p)
   {
      // placeholder not found
      return string;
   }

   len = p - string;
   strncpy(buffer, string, len); 
   buffer[len] = 0;

   sprintf(buffer + len, "%s%s", value, p + strlen(placeholder));

   return buffer;
}

/**
 * Redirect STDOUT to an output file. 
 */
void stdout_redirect()
{
   int output_fd; 
   
   /* making output file name with PID */
   sprintf(log_file_name, "spawn_native_%u.log", getpid());
    
   // Open file
   output_fd = open(log_file_name, O_WRONLY | O_CREAT | O_APPEND, 0644);
   
   if (output_fd == -1)
   {
      fprintf(stderr, "Cannot open file %s\n", log_file_name);
   }
   else
   {
      /* Redirect STDOUT to file */
      if (dup2(output_fd, STDOUT_FILENO) == -1)
      {
         close(output_fd);
         fprintf(stderr, "Cannot redirect STDOUT to file %s\n", log_file_name);
      }
   }
}

/**
 * Main function which allows both password and P2J server-style authentication.
 * <p>
 * For P2J server-style authentiation, the following files need to exist in current directory:
 * - p2j.jar, which will automatically be added to classpath
 * - srv-certs.store, a key store with the certificates for the valid P2J server which can invoke
 * this utility.
 *
 * Link option: -lm -lcrypt -lpam -ljvm
 * <p>
 * After build do the following:
 *    $ sudo chown root:root spawn
 *    $ sudo chmod 4755 spawn
 *    $ sudo chmod 0440 srv-certs.store
 *    $ sudo chmod 0440 p2j.jar
 *
 * @param    argc
 *           Number of command line arguments.
 * @param    argv
 *           Command line arguments.
 *           
 * @return   Exit status.
 */
int main(int argc, char *argv[])
{
   int result, i;
   char pid[32];
  
   /* convert pid to string */
   sprintf(pid, "%u", getpid()); 
   sprintf(message_buffer, "Spawn process started with pid %s.", pid);
   logMessage(message_buffer);

   /* Init output file name buffer */
   output_file_name[0] = 0;
   
   /* Check for root permissions */
   if (check_root(argv[0], 04755))
   {
      fputs("SEVERE: No root access permissions.\n", stderr);
      exit(ERR_SPAWN_NO_ROOT_PERMISSION);
   }

   /* Print a short help */
   if (argc == 1)
   {
      help(argv[0]);
      exit(ERR_SPAWN_NO_ARGS);
   }

   char *smode = argv[1];
   if (strcmp(smode, "1") == 0 || strcmp(smode, "2") == 0)
   {
      int mode1 = strcmp(smode, "1");
      int mode2 = strcmp(smode, "2");
      
      if ((mode1 == 0 && argc < MIN_ARGS_PASSWORD) || (mode2 == 0 && argc < MIN_ARGS_CREDS))
      {
         help(argv[0]);
         exit(ERR_SPAWN_NO_ARGS);
      }

      /* Ask for password */
      size_t len;
      char *psw = password(&len);
   
      if (psw == NULL)
      {
         fputs("SEVERE: No password.\n", stderr);
         exit(ERR_SPAWN_NO_PASSWORD);
      }

      /* Check user account credentials */
      result = check_user(argv[2], psw);
      
      /* Password is no longer needed at this point, zero it so it doesn't stay in memory. */
      zap(&psw, &len);

      switch (result)
      {
         case AUTH_OK:
            if (mode1 == 0)
            {
               // we need to consider possible one more extra parameter before java arguments
               sprintf(message_buffer,
                       "Spawn cmd (%s) in workdir (%s).",
                       argv[MIN_ARGS_PASSWORD],
                       argv[MIN_ARGS_PASSWORD - 1]);
               logMessage(message_buffer);
               result = spawn(argv[MIN_ARGS_PASSWORD],
                              &argv[MIN_ARGS_PASSWORD],
                              argv[MIN_ARGS_PASSWORD - 1]);
            }
            break;
         case AUTH_ERR_USER:
            fputs("Invalid user name.\n", stderr);
            break;
         case AUTH_ERR_PASSWORD:
            fputs("Invalid password.\n", stderr);
            break;
         case AUTH_ERR_ROOT:
            fputs("User root not allowed.\n", stderr);
            break;
          case AUTH_ERR_NULL_ACCOUNT_PASSWORD:
            fputs("No password set for user account.\n", stderr);
            break;
        case AUTH_ERR_CRYPT:
            fputs("Password encryption error.\n", stderr);
            break;
      }
      sprintf(message_buffer, "Spawn result code: (%d).", result);
      logMessage(message_buffer);
   }
   else if (strcmp(smode, "0") == 0)
   {
      if (check_root(KEY_STORE, 00550))
      {
         fputs("No root access permissions for srv-certs.store.\n", stderr);
         exit(ERR_SPAWN_NO_ROOT_PERMISSION);
      }
      if (check_root(P2J_JAR, 00550))
      {
         fputs("No root access permissions for p2j.jar.\n", stderr);
         exit(ERR_SPAWN_NO_ROOT_PERMISSION);
      }
      
      if (argc < MIN_ARGS_NO_PASSWORD)
      {
         help(argv[0]);
         exit(ERR_SPAWN_NO_ARGS);
      }
      
      /* Search for optional parameters */
      for (i = MIN_ARGS_NO_PASSWORD; i < argc; i++)
      {
         /* -O <outputToFile> */
         if ((strcmp("-O", argv[i]) == 0) && (++i < argc))
         {
            strcpy(output_file_name, argv[i]);
            output_to_file_opt_found = 1;
         }

         /* -L switch to mmake native errors logging */
         if (strcmp("-L", argv[i]) == 0)
         {
            native_log_found = 1;
         }
      }

      char* misc = (argc < 7) ? "" : argv[6];
      // when the mode is 0 the extra debug option is located after 2-5 parameters so no need
      // to shift indexes for parameters in call below, it is safe
      result = launchP2JClient(argv[2], argv[3], argv[4], argv[5], misc);
   }
   else
   {
      help(argv[0]);
      exit(ERR_SPAWN_NO_ARGS);
   }

   exit(result);
}
