lcpex rewrite - alpha

This commit is contained in:
phanes
2023-02-15 18:44:21 -05:00
parent bb85754dc0
commit 7ed6e13fa5
82 changed files with 4510 additions and 2592 deletions

70
src/lcpex/Contexts.cpp Normal file
View File

@@ -0,0 +1,70 @@
#include "Contexts.h"
// converts username to UID
int username_to_uid( std::string username, int & uid )
{
// assume failure unless proven otherwise
int r_code = false;
struct passwd * pw;
if ( ( pw = getpwnam( username.c_str() ) ) != NULL )
{
// successful user lookup
r_code = true;
uid = pw->pw_uid;
} else {
// failed lookup, do nothing, assumed failure
}
return r_code;
};
// converts group name to GID
int groupname_to_gid( std::string groupname, int & gid )
{
int r_code = false;
struct group * gp;
if ( ( gp = getgrnam( groupname.c_str() ) ) != NULL )
{
r_code = true;
gid = gp->gr_gid;
} else {
// failed lookup, do nothing, assumed failure
}
return r_code;
}
// SET PROCESS TO A CERTAIN IDENTITY CONTEXT
int set_identity_context( std::string user_name, std::string group_name ) {
// the UID and GID for the username and groupname provided for context setting
int context_user_id;
int context_group_id;
int res = 0;
// convert username to UID
if (! ( res = username_to_uid(user_name, context_user_id ) ) )
{
return ERROR_NO_SUCH_USER;
}
// convert group name to GID
if (! ( res = groupname_to_gid(group_name, context_group_id ) ) )
{
return ERROR_NO_SUCH_GROUP;
}
if ( ( res = setgid(context_group_id) ) ) {
perror("lcpex: setgid failed");
return ERROR_SETGID_FAILED;
}
if ( ( res = setuid(context_user_id) ) ) {
perror("lcpex: setuid failed");
return ERROR_SETUID_FAILED;
}
return ERROR_NONE;
}

69
src/lcpex/Contexts.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef LCPEX_CONTEXTS_H
#define LCPEX_CONTEXTS_H
#include <string>
#include <csignal>
#include <pwd.h>
#include <grp.h>
#include <iostream>
enum IDENTITY_CONTEXT_ERRORS {
ERROR_NONE = 0,
ERROR_NO_SUCH_USER,
ERROR_NO_SUCH_GROUP,
ERROR_NO_SUCH_USER_IN_GROUP,
ERROR_SETGID_FAILED,
ERROR_SETUID_FAILED,
};
/**
* @brief Converts a username to a user ID (UID)
*
* @param username The username to convert
* @param uid A reference to the resulting UID
*
* @return A boolean indicating whether the conversion was successful
*
* This function takes a username as a string and returns the corresponding UID in the `uid` parameter.
* The function returns a boolean indicating whether the conversion was successful.
* If the username is not found, the function returns false and the value of `uid` is unchanged.
*/
int username_to_uid( std::string username, int & uid );
/**
* @brief Converts a group name to a group ID (GID)
*
* @param groupname The group name to convert
* @param gid A reference to the resulting GID
*
* @return A boolean indicating whether the conversion was successful
*
* This function takes a group name as a string and returns the corresponding GID in the `gid` parameter.
* The function returns a boolean indicating whether the conversion was successful.
* If the group name is not found, the function returns false and the value of `gid` is unchanged.
*/
int groupname_to_gid( std::string groupname, int & gid );
/**
* @brief Sets the execution context for a process
*
* @param user_name The username to use for the execution context
* @param group_name The group name to use for the execution context
*
* @return An error code indicating the result of the context setting
*
* This function takes a username and group name as input and sets the execution context for the process.
* The function converts the username and group name to UID and GID respectively, and sets the process's
* user and group identity using the setuid() and setgid() functions.
*
* If either the username or group name is not found, the function returns `ERROR_NO_SUCH_USER` or `ERROR_NO_SUCH_GROUP` respectively.
* If the call to setgid() fails, the function returns `ERROR_SETGID_FAILED`.
* If the call to setuid() fails, the function returns `ERROR_SETUID_FAILED`.
* If the context setting is successful, the function returns `ERROR_NONE`.
*/
int set_identity_context( std::string user_name, std::string group_name );
#endif //LCPEX_CONTEXTS_H

21
src/lcpex/helpers.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef LCPEX_HELPERS_H
#define LCPEX_HELPERS_H
// helper for sanity
enum PIPE_ENDS {
READ_END = 0,
WRITE_END = 1
};
// helper for sanity
// these do NOT represent fd values
enum CHILD_PIPE_NAMES {
STDOUT_READ = 0,
STDERR_READ = 1
};
#define BUFFER_SIZE 1024
#endif //LCPEX_HELPERS_H

338
src/lcpex/liblcpex.cpp Normal file
View File

@@ -0,0 +1,338 @@
#include "liblcpex.h"
std::string prefix_generator(
std::string command,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
) {
std::string prefix = "";
if ( is_shell_command ) {
// it's a shell command, so we need to set up a shell execution of the command
prefix = shell_path;
// if the shell takes an argument to execute a command, add it enclosed in quotes
if (shell_execution_arg != "") {
// add the execution arg
prefix += " " + shell_execution_arg + " ";
} else {
// no execution arg, so add a space
prefix += " ";
}
if (supply_environment) {
// add the source subcommand
prefix += "'" + shell_source_subcommand + " " + environment_file_path + " && " + command + "'";
} else {
// no environment supplied, so just add the command
prefix += "'" + command + "'";
}
} else {
// it's not a shell command, so we can just execute it directly
prefix = command;
}
std::cout << "prefix: " << prefix << std::endl;
return prefix;
}
int lcpex(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool force_pty,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
) {
// generate the prefix
std::string prefix = prefix_generator(
command,
is_shell_command,
shell_path,
shell_execution_arg,
supply_environment,
shell_source_subcommand,
environment_file_path
);
command = prefix;
// if we are forcing a pty, then we will use the vpty library
if( force_pty )
{
return exec_pty( command, stdout_log_file, stderr_log_file, context_override, context_user, context_group, supply_environment );
}
// otherwise, we will use the execute function
return execute( command, stdout_log_file, stderr_log_file, context_override, context_user, context_group, supply_environment );
}
/**
* @brief Set the close-on-exec flag of a file descriptor
*
* This function uses the `fcntl` function to set the close-on-exec flag
* of the specified file descriptor `fd`. If the `fcntl` call fails,
* the function will print an error message to `stderr` and exit with status 1.
*
* @param fd The file descriptor for which to set the close-on-exec flag
*/
void set_cloexec_flag(int fd)
{
if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1)
{
perror("fcntl");
exit(1);
}
}
/**
* @brief This function is executed by the child process to run a shell command
*
* @param context_override Indicates whether to override the current execution context
* @param context_user The user to switch to for execution context
* @param context_group The group to switch to for execution context
* @param processed_command The command to be executed, after processing
* @param fd_child_stdout_pipe The file descriptor for the child process's standard output pipe
* @param fd_child_stderr_pipe The file descriptor for the child process's standard error pipe
*
* The run_child_process() function takes the parameters context_override, context_user, context_group, processed_command,
* fd_child_stdout_pipe and fd_child_stderr_pipe.
*
* If context_override is set to true, the child process will run under the specified context_user and context_group.
* If either the context_user or context_group does not exist, an error message will be displayed and the process will exit.
*
* The function first redirects the child process's standard output and standard error to pipes.
*
* If context_override is set to true, the child process sets its identity context using the set_identity_context() function.
* If an error occurs while setting the identity context, a message will be displayed and the process will exit.
*
* Finally, the child process calls execvp() with the processed_command to run the shell command.
* If the execvp() function fails, an error message is displayed.
*/
void run_child_process(bool context_override, const char* context_user, const char* context_group, char* processed_command[], int fd_child_stdout_pipe[], int fd_child_stderr_pipe[]) {
while ((dup2(fd_child_stdout_pipe[WRITE_END], STDOUT_FILENO) == -1) && (errno == EINTR)) {}
while ((dup2(fd_child_stderr_pipe[WRITE_END], STDERR_FILENO) == -1) && (errno == EINTR)) {}
if ( context_override ) {
int context_result = set_identity_context(context_user, context_group);
switch (context_result) {
case IDENTITY_CONTEXT_ERRORS::ERROR_NONE:
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_USER:
std::cerr << "lcpex: Aborting: context user not found: " << context_user << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_GROUP:
std::cerr << "lcpex: Aborting: context group not found: " << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETGID_FAILED:
std::cerr << "lcpex: Aborting: Setting GID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETUID_FAILED:
std::cerr << "lcpex: Aborting: Setting UID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
default:
std::cerr << "lcpex: Aborting: Unknown error while setting identity context." << std::endl;
exit(1);
break;
}
}
int exit_code = execvp(processed_command[0], processed_command);
perror("failed on execvp in child");
exit(exit_code);
}
int execute(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
){
// this does three things:
// - execute a dang string as a subprocess command
// - capture child stdout/stderr to respective log files
// - TEE child stdout/stderr to parent stdout/stderr
// turn our command string into something execvp can consume
char ** processed_command = expand_env(command );
// if the user chose to supply the environment, then we need to clear the environment
// before we fork the process, so that the child process will inherit the environment
// from the parent process
if ( environment_supplied ) {
clearenv();
}
// open file handles to the two log files we need to create for each execution
FILE * stdout_log_fh = fopen( stdout_log_file.c_str(), "a+" );
FILE * stderr_log_fh = fopen( stderr_log_file.c_str(), "a+" );
// create the pipes for the child process to write and read from using its stdin/stdout/stderr
int fd_child_stdout_pipe[2];
int fd_child_stderr_pipe[2];
if ( pipe(fd_child_stdout_pipe ) == -1 ) {
perror( "child stdout pipe" );
exit( 1 );
}
if ( pipe( fd_child_stderr_pipe ) == -1 ) {
perror( "child stderr pipe" );
exit( 1 );
}
// using O_CLOEXEC to ensure that the child process closes the file descriptors
set_cloexec_flag( fd_child_stdout_pipe[READ_END] );
set_cloexec_flag( fd_child_stderr_pipe[READ_END] );
set_cloexec_flag( fd_child_stdout_pipe[WRITE_END] );
set_cloexec_flag( fd_child_stderr_pipe[WRITE_END] );
// status result basket for the parent process to capture the child's exit status
int status = 616;
pid_t pid = fork();
switch( pid ) {
case -1:
{
// fork failed
perror("fork failure");
exit(1);
}
case 0:
{
// child process
run_child_process(
context_override,
context_user.c_str(),
context_group.c_str(),
processed_command,
fd_child_stdout_pipe,
fd_child_stderr_pipe
);
}
default:
{
// parent process
// The parent process has no need to access the entrance to the pipe, so fd_child_*_pipe[1|0] should be closed
// within that process too:
close(fd_child_stdout_pipe[WRITE_END]);
close(fd_child_stderr_pipe[WRITE_END]);
// attempt to write to stdout,stderr from child as well as to write each to file
char buf[BUFFER_SIZE];
// contains the byte count of the last read from the pipe
ssize_t byte_count;
// watched_fds for poll() to wait on
struct pollfd watched_fds[2];
// populate the watched_fds array
// child STDOUT to parent
watched_fds[CHILD_PIPE_NAMES::STDOUT_READ].fd = fd_child_stdout_pipe[READ_END];
watched_fds[CHILD_PIPE_NAMES::STDOUT_READ].events = POLLIN;
// child STDERR to parent
watched_fds[CHILD_PIPE_NAMES::STDERR_READ].fd = fd_child_stderr_pipe[READ_END];
watched_fds[CHILD_PIPE_NAMES::STDERR_READ].events = POLLIN;
// number of files poll() reports as ready
int num_files_readable;
// loop flag
bool break_out = false;
// loop until we've read all the data from the child process
while ( ! break_out ) {
num_files_readable = poll(watched_fds, sizeof(watched_fds) / sizeof(watched_fds[0]), -1);
if (num_files_readable == -1) {
// error occurred in poll()
perror("poll");
exit(1);
}
if (num_files_readable == 0) {
// poll reports no files readable
break_out = true;
break;
}
for (int this_fd = 0; this_fd < 2; this_fd++) {
if (watched_fds[this_fd].revents & POLLIN) {
// this pipe is readable
byte_count = read(watched_fds[this_fd].fd, buf, BUFFER_SIZE);
if (byte_count == -1) {
// error reading from pipe
perror("read");
exit(EXIT_FAILURE);
} else if (byte_count == 0) {
// reached EOF on one of the streams but not a HUP
// we've read all we can this cycle, so go to the next fd in the for loop
continue;
} else {
// byte count was sane
// write to stdout,stderr
if (this_fd == CHILD_PIPE_NAMES::STDOUT_READ) {
// the child's stdout pipe is readable
write(stdout_log_fh->_fileno, buf, byte_count);
write(STDOUT_FILENO, buf, byte_count);
} else if (this_fd == CHILD_PIPE_NAMES::STDERR_READ) {
// the child's stderr pipe is readable
write(stderr_log_fh->_fileno, buf, byte_count);
write(STDERR_FILENO, buf, byte_count);
} else {
// this should never happen
perror("Logic error!");
exit(EXIT_FAILURE);
}
}
}
if (watched_fds[this_fd].revents & POLLERR) {
close(watched_fds[this_fd].fd);
break_out = true;
}
if (watched_fds[this_fd].revents & POLLHUP) {
// this pipe has hung up
close(watched_fds[this_fd].fd);
break_out = true;
}
}
}
// wait for child to exit, capture status
waitpid(pid, &status, 0);
// close the log file handles
fclose(stdout_log_fh);
fclose(stderr_log_fh);
if WIFEXITED(status) {
return WEXITSTATUS(status);
} else {
return -617;
}
}
}
}

122
src/lcpex/liblcpex.h Normal file
View File

@@ -0,0 +1,122 @@
#ifndef LCPEX_LIBLCPEX_H
#define LCPEX_LIBLCPEX_H
#include <cstdio>
#include <cstdlib>
#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
#include "string_expansion/string_expansion.h"
#include "helpers.h"
#include "Contexts.h"
#include "vpty/pty_fork_mod/tty_functions.h"
#include "vpty/pty_fork_mod/pty_fork.h"
#include "vpty/libclpex_tty.h"
/**
* @brief This function is executed by the child process to run a shell command
*
* @param context_override Indicates whether to override the current execution context
* @param context_user The user to switch to for execution context
* @param context_group The group to switch to for execution context
* @param processed_command The command to be executed, after processing
* @param fd_child_stdout_pipe The file descriptor for the child process's standard output pipe
* @param fd_child_stderr_pipe The file descriptor for the child process's standard error pipe
*
* If context_override is set to true, the child process will run under the specified context_user and context_group.
* If either the context_user or context_group does not exist, an error message will be displayed and the process will exit.
*
* The function first redirects the child process's standard output and standard error to pipes.
*
* If context_override is set to true, the child process sets its identity context using the set_identity_context() function.
* If an error occurs while setting the identity context, a message will be displayed and the process will exit.
*
* Finally, the child process calls execvp() with the processed_command to run the shell command.
* If the execvp() function fails, an error message is displayed.
*/
int execute(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
);
/**
* @brief Executes a command with logging and optional context switching
*
* @param command The command to be executed
* @param stdout_log_file The file to log the standard output of the command
* @param stderr_log_file The file to log the standard error of the command
* @param context_override Indicates whether to override the current execution context
* @param context_user The user to switch to for execution context
* @param context_group The group to switch to for execution context
* @param force_pty Indicates whether to force a pseudoterminal (pty) for the command execution
* @param is_shell_command Indicates whether the command is a shell command
* @param shell_path The path to the shell executable
* @param shell_execution_arg The argument used to execute a command in the shell
* @param supply_environment Indicates whether to supply an environment
* @param shell_source_subcommand The shell subcommand used to source the environment file
* @param environment_file_path The path to the environment file
*
* @return The exit status of the executed command
*
* This function executes a command with logging and optional context switching. The function generates
* a prefix for the command using the `prefix_generator` function, which sets up a shell execution if
* the command is a shell command. If a pseudoterminal (pty) is forced, the `exec_pty` function is used
* to execute the command, otherwise the `execute` function is used. The standard output and standard
* error of the command are logged to the provided log files. If context overriding is requested, the
* execution context is switched to the provided user and group.
*/
int lcpex(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool force_pty,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
);
/**
* @brief Generates a command prefix for execution
*
* @param command The main command to be executed
* @param is_shell_command Indicates whether the command is a shell command
* @param shell_path The path to the shell executable
* @param shell_execution_arg The argument used to execute a command in the shell
* @param supply_environment Indicates whether to supply an environment
* @param shell_source_subcommand The shell subcommand used to source the environment file
* @param environment_file_path The path to the environment file
*
* @return The generated command prefix
*
* This function generates a prefix for a command to be executed, based on the provided parameters.
* If the command is a shell command, the prefix will be set up for a shell execution. If the shell
* takes an argument to execute a command, it will be added to the prefix. If an environment is to be
* supplied, the shell subcommand for sourcing the environment file and the environment file path will
* be added to the prefix. If the command is not a shell command, the command will be returned as is.
*/
std::string prefix_generator(
std::string command,
bool is_shell_command,
std::string shell_path,
std::string shell_execution_arg,
bool supply_environment,
std::string shell_source_subcommand,
std::string environment_file_path
);
#endif //LCPEX_LIBLCPEX_H

View File

@@ -0,0 +1,24 @@
#include "string_expansion.h"
// convert a string to a char** representing our artificial argv to be consumed by execvp
char ** expand_env(const std::string& var, int flags )
{
std::vector<std::string> vars;
wordexp_t p;
if(!wordexp(var.c_str(), &p, flags))
{
if(p.we_wordc)
for(char** exp = p.we_wordv; *exp; ++exp)
vars.push_back(exp[0]);
wordfree(&p);
}
char ** arr = new char*[vars.size() + 1];
for(size_t i = 0; i < vars.size(); i++){
arr[i] = new char[vars[i].size() + 1];
strcpy(arr[i], vars[i].c_str());
}
arr[vars.size()] = (char * ) nullptr;
return arr;
}

View File

@@ -0,0 +1,29 @@
#ifndef LCPEX_STRING_EXPANSION_H
#define LCPEX_STRING_EXPANSION_H
#include <wordexp.h>
#include <vector>
#include <cstring>
#include <iostream>
/**
* @brief Convert a string to a 'char**' representing an argument list
*
* This function takes a string 'var' and converts it into a 'char**'
* representing an argument list. The argument list can be consumed by
* functions like 'execvp'.
*
* @param[in] var The string to be converted
* @param[in] flags Flags controlling the behavior of the conversion
*
* @return A 'char**' representing the argument list
*
* The function uses the 'wordexp' function to perform the conversion. The
* 'flags' argument controls the behavior of the 'wordexp' function. The
* returned 'char**' is dynamically allocated, and must be freed by the
* caller using 'delete[]'.
*/
char ** expand_env(const std::string& var, int flags = 0);
#endif //LCPEX_STRING_EXPANSION_H

View File

@@ -0,0 +1,282 @@
#include "libclpex_tty.h"
/**
* @brief Safely prints a message and exits the program
*
* @param msg The message to print
* @param ttyOrig A pointer to a struct termios representing the original terminal settings
*
* This function takes a message `msg` and a pointer to a struct termios `ttyOrig` as input.
* The function first prints the `msg` to `stderr`.
* Then, it calls `ttyResetExit()` with `ttyOrig` as its argument to reset the terminal settings.
* Finally, the function exits the program with exit code 1.
*/
void safe_perror( const char * msg, struct termios * ttyOrig )
{
std::cerr << msg << std::endl;
ttyResetExit( ttyOrig );
exit(1);
}
/**
* @brief Executes the child process
*
* @param fd_child_stderr_pipe A file descriptor array for the child process's stderr pipe
* @param processed_command An array of char pointers representing the command and its arguments to be executed
* @param ttyOrig A pointer to a struct termios representing the original terminal settings
* @param context_override A flag indicating whether to override the process's execution context
* @param context_user The username to use for the execution context if `context_override` is `true`
* @param context_group The group name to use for the execution context if `context_override` is `true`
*
* This function takes an array of file descriptors `fd_child_stderr_pipe` for the child process's stderr pipe,
* an array of char pointers `processed_command` representing the command and its arguments to be executed,
* a pointer to a struct termios `ttyOrig` representing the original terminal settings,
* a flag `context_override` indicating whether to override the process's execution context,
* a string `context_user` representing the username to use for the execution context if `context_override` is `true`,
* and a string `context_group` representing the group name to use for the execution context if `context_override` is `true`.
*
* The function first redirects the child process's stderr to the write end of the stderr pipe.
* If `context_override` is `true`, the function sets the process's execution context using `set_identity_context()`.
* If `context_override` is `false`, the function does nothing.
* Finally, the function executes the command specified in `processed_command` using `execvp()`.
* If the execution of `execvp()` fails, the function calls `safe_perror()` to print a message and exit the program.
*/
void run_child_process( int fd_child_stderr_pipe[2], char * processed_command[], struct termios * ttyOrig, bool context_override, std::string context_user, std::string context_group )
{
// redirect stderr to the write end of the stderr pipe
// close the file descriptor STDERR_FILENO if it was previously open, then (re)open it as a copy of
while ((dup2(fd_child_stderr_pipe[WRITE_END], STDERR_FILENO) == -1) && (errno == EINTR)) {}
// close the write end of the stderr pipe
close( fd_child_stderr_pipe[WRITE_END] );
// if the user has specified a context override, set the context
if ( context_override )
{
int context_result = set_identity_context(context_user, context_group);
switch (context_result) {
case IDENTITY_CONTEXT_ERRORS::ERROR_NONE:
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_USER:
std::cerr << "lcpex: Aborting: context user not found: " << context_user << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_NO_SUCH_GROUP:
std::cerr << "lcpex: Aborting: context group not found: " << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETGID_FAILED:
std::cerr << "lcpex: Aborting: Setting GID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
case IDENTITY_CONTEXT_ERRORS::ERROR_SETUID_FAILED:
std::cerr << "lcpex: Aborting: Setting UID failed: " << context_user << "/" << context_group << std::endl;
exit(1);
break;
default:
std::cerr << "lcpex: Aborting: Unknown error while setting identity context." << std::endl;
exit(1);
break;
}
}
// execute the dang command, print to stdout, stderr (of parent), and dump to file for each!!!!
// (and capture exit code in parent)
int exit_code = execvp(processed_command[0], processed_command);
safe_perror("failed on execvp in child", ttyOrig );
exit(exit_code);
}
// this does three things:
// - execute a dang string as a subprocess command
// - capture child stdout/stderr to respective log files
// - TEE child stdout/stderr to parent stdout/stderr
int exec_pty(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
) {
// initialize the terminal settings obj
struct termios ttyOrig;
// if the user chose to supply the environment, then we need to clear the environment
// before we fork the process, so that the child process will inherit the environment
// from the parent process
if ( environment_supplied ) {
clearenv();
}
// turn our command string into something execvp can consume
char ** processed_command = expand_env(command );
// open file handles to the two log files we need to create for each execution
FILE * stdout_log_fh = fopen( stdout_log_file.c_str(), "a+" );
FILE * stderr_log_fh = fopen( stderr_log_file.c_str(), "a+" );
// create the pipes for the child process to write and read from using its stderr
int fd_child_stderr_pipe[2];
if ( pipe( fd_child_stderr_pipe ) == -1 ) {
safe_perror( "child stderr pipe", &ttyOrig );
exit( 1 );
}
// using O_CLOEXEC to ensure that the child process closes the file descriptors
if ( fcntl( fd_child_stderr_pipe[READ_END], F_SETFD, FD_CLOEXEC ) == -1 ) { perror("fcntl"); exit(1); }
// // same for stderr write
if ( fcntl( fd_child_stderr_pipe[WRITE_END], F_SETFD, FD_CLOEXEC ) == -1 ) { perror("fcntl"); exit(1); }
// status result basket for the parent process to capture the child's exit status
int status = 616;
// start ptyfork integration
char slaveName[MAX_SNAME];
int masterFd;
struct winsize ws;
/* Retrieve the attributes of terminal on which we are started */
if (tcgetattr(STDIN_FILENO, &ttyOrig) == -1)
safe_perror("tcgetattr", &ttyOrig);
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) < 0)
safe_perror("ioctl-TIOCGWINSZ", &ttyOrig );
pid_t pid = ptyFork( &masterFd, slaveName, MAX_SNAME, &ttyOrig, &ws );
switch( pid ) {
case -1:
{
// fork failed
safe_perror("ptyfork failure", &ttyOrig );
exit(1);
}
case 0:
{
// child process
run_child_process( fd_child_stderr_pipe, processed_command, &ttyOrig, context_override, context_user, context_group );
}
default:
{
// parent process
// start ptyfork integration
ttySetRaw(STDIN_FILENO, &ttyOrig);
// The parent process has no need to access the entrance to the pipe
close(fd_child_stderr_pipe[WRITE_END]);
// attempt to write to stdout,stderr from child as well as to write each to file
char buf[BUFFER_SIZE];
// contains the byte count of the last read from the pipe
ssize_t byte_count;
// watched_fds for poll() to wait on
struct pollfd watched_fds[3];
// populate the watched_fds array
// child in to parent
watched_fds[0].fd = STDIN_FILENO;
watched_fds[0].events = POLLIN;
// child pty to parent (stdout)
watched_fds[1].fd = masterFd;
watched_fds[1].events = POLLIN;
// child stderr to parent
watched_fds[2].fd = fd_child_stderr_pipe[READ_END];
watched_fds[2].events = POLLIN;
// number of files poll() reports as ready
int num_files_readable;
// loop flag
bool break_out = false;
// loop until we've read all the data from the child process
while ( ! break_out ) {
num_files_readable = poll(watched_fds, sizeof(watched_fds) / sizeof(watched_fds[0]), -1);
if (num_files_readable == -1) {
// error occurred in poll()
safe_perror("poll", &ttyOrig );
exit(1);
}
if (num_files_readable == 0) {
// poll reports no files readable
break_out = true;
break;
}
for (int this_fd = 0; this_fd < 3; this_fd++) {
if (watched_fds[this_fd].revents & POLLIN) {
// this pipe is readable
byte_count = read(watched_fds[this_fd].fd, buf, BUFFER_SIZE);
if (byte_count == -1) {
// error reading from pipe
safe_perror("read", &ttyOrig );
exit(EXIT_FAILURE);
} else if (byte_count == 0) {
// reached EOF on one of the streams but not a HUP
// we've read all we can this cycle, so go to the next fd in the for loop
continue;
} else {
// byte count was sane
// write to stdout,stderr
if (this_fd == 0) {
// parent stdin received, write to child pty (stdin)
write(masterFd, buf, byte_count);
} else if (this_fd == 1 ) {
// child pty sent some stuff, write to parent stdout and log
write(stdout_log_fh->_fileno, buf, byte_count);
write(STDOUT_FILENO, buf, byte_count);
} else if ( this_fd == 2 ){
//the child's stderr pipe sent some stuff, write to parent stderr and log
write(stderr_log_fh->_fileno, buf, byte_count);
write(STDERR_FILENO, buf, byte_count);
} else {
// this should never happen
perror("Logic error!");
exit(EXIT_FAILURE);
}
}
}
if (watched_fds[this_fd].revents & POLLERR) {
close(watched_fds[this_fd].fd);
//break_out = true;
continue;
}
if (watched_fds[this_fd].revents & POLLHUP) {
// this pipe has hung up
close(watched_fds[this_fd].fd);
break_out = true;
break;
}
}
}
// wait for child to exit, capture status
waitpid(pid, &status, 0);
// close the log file handles
fclose(stdout_log_fh);
fclose(stderr_log_fh);
ttyResetExit( &ttyOrig);
if WIFEXITED(status) {
return WEXITSTATUS(status);
} else {
return -617;
}
}
}
}

View File

@@ -0,0 +1,50 @@
//
// Created by phanes on 2/6/23.
//
#ifndef LCPEX_LIBCLPEX_TTY_H
#define LCPEX_LIBCLPEX_TTY_H
#include <cstdio>
#include <cstdlib>
#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
#include "../string_expansion/string_expansion.h"
#include "../helpers.h"
#include "../Contexts.h"
#include "../vpty/pty_fork_mod/tty_functions.h"
#include "../vpty/pty_fork_mod/pty_fork.h"
#include <sys/ioctl.h>
#include <string>
/**
* @brief Execute a string as a subprocess command, capture its stdout/stderr to log files, and TEE its output to the parent process's stdout/stderr.
* This function performs the following three tasks:
* - Execute a string as a subprocess command.
* - Capture the child process's stdout and stderr to respective log files.
* - TEE the child process's stdout and stderr to the parent process's stdout and stderr.
*
* @param command The command to be executed as a subprocess.
* @param stdout_log_file The file path where the child process's stdout will be logged.
* @param stderr_log_file The file path where the child process's stderr will be logged.
* @param context_override Specify whether to override the process's execution context.
* @param context_user The user context to run the process as, if context_override is true.
* @param context_group The group context to run the process as, if context_override is true.
* @param environment_supplied Specify whether the environment is supplied.
* @return The exit status of the child process. If the child process terminated due to a signal, returns -617.
*/
int exec_pty(
std::string command,
std::string stdout_log_file,
std::string stderr_log_file,
bool context_override,
std::string context_user,
std::string context_group,
bool environment_supplied
);
#endif //LCPEX_LIBCLPEX_TTY_H

View File

@@ -0,0 +1,79 @@
#include "pty_fork.h"
pid_t ptyFork(int *masterFd, char *slaveName, size_t snLen,
const struct termios *slaveTermios, const struct winsize *slaveWS)
{
int mfd, slaveFd, savedErrno;
pid_t childPid;
char slname[MAX_SNAME];
mfd = ptyMasterOpen(slname, MAX_SNAME);
if (mfd == -1)
return -1;
if (slaveName != NULL) { /* Return slave name to caller */
if (strlen(slname) < snLen) {
strncpy(slaveName, slname, snLen);
} else { /* 'slaveName' was too small */
close(mfd);
errno = EOVERFLOW;
return -1;
}
}
childPid = fork();
/* fork() failed */
if (childPid == -1) {
/* close() might change 'errno' */
savedErrno = errno;
/* Don't leak file descriptors */
close(mfd);
errno = savedErrno;
return -1;
}
if (childPid != 0) { /* Parent */
*masterFd = mfd; /* Only parent gets master fd */
return childPid; /* Like parent of fork() */
}
/* Child falls through to here */
if (setsid() == -1) /* Start a new session */
perror("ptyFork:setsid");
close(mfd); /* Not needed in child */
slaveFd = open(slname, O_RDWR); /* Becomes controlling tty */
if (slaveFd == -1)
perror("ptyFork:open-slave");
#ifdef TIOCSCTTY /* Acquire controlling tty on BSD */
if (ioctl(slaveFd, TIOCSCTTY, 0) == -1)
perror("ptyFork:ioctl-TIOCSCTTY");
#endif
if (slaveTermios != NULL) /* Set slave tty attributes */
if (tcsetattr(slaveFd, TCSANOW, slaveTermios) == -1)
perror("ptyFork:tcsetattr");
if (slaveWS != NULL) /* Set slave tty window size */
if (ioctl(slaveFd, TIOCSWINSZ, slaveWS) == -1)
perror("ptyFork:ioctl-TIOCSWINSZ");
/* Duplicate pty slave to be child's stdin, stdout, and stderr */
if (dup2(slaveFd, STDIN_FILENO) != STDIN_FILENO)
perror("ptyFork:dup2-STDIN_FILENO");
if (dup2(slaveFd, STDOUT_FILENO) != STDOUT_FILENO)
perror("ptyFork:dup2-STDOUT_FILENO");
if (dup2(slaveFd, STDERR_FILENO) != STDERR_FILENO)
perror("ptyFork:dup2-STDERR_FILENO");
if (slaveFd > STDERR_FILENO) /* Safety check */
close(slaveFd); /* No longer need this fd */
return 0; /* Like child of fork() */
}

View File

@@ -0,0 +1,39 @@
#ifndef LCPEX_PTY_FORK_H
#define LCPEX_PTY_FORK_H
#include <sys/types.h>
#include "./pty_master_open.h"
#include <cstdio>
#include "./tty_functions.h"
#include <sys/ioctl.h>
/* Maximum size for pty slave name */
#define MAX_SNAME 1000
/**
* @brief Fork a child process with a pseudo-terminal
*
* This function forks a child process and returns the child's process ID. The
* child process is attached to a pseudo-terminal and its standard input,
* output, and error streams are redirected to the slave side of the pseudo-terminal.
*
* @param[out] masterFd Pointer to store the file descriptor of the master pty
* @param[out] slaveName Buffer to store the name of the slave pty
* @param[in] snLen Length of the 'slaveName' buffer
* @param[in] slaveTermios Terminal attributes for the slave pty (optional)
* @param[in] slaveWS Window size for the slave pty (optional)
*
* @return Process ID of the child process on success, or -1 on error
*
* On success, the file descriptor of the master pty is stored in the location
* pointed to by 'masterFd', and the name of the slave pty is stored in the buffer
* pointed to by 'slaveName'. If the buffer is too small to hold the name of the
* slave, an error of type 'EOVERFLOW' is returned. If 'slaveTermios' is non-NULL,
* the terminal attributes specified by it will be set for the slave pty. If
* 'slaveWS' is non-NULL, the window size specified by it will be set for the slave
* pty.
*/
pid_t ptyFork(int *masterFd, char *slaveName, size_t snLen, const struct termios *slaveTermios, const struct winsize *slaveWS);
#endif //LCPEX_PTY_FORK_H

View File

@@ -0,0 +1,76 @@
/* pty_master_open.cPP
Implement our ptyMasterOpen() function, based on UNIX 98 pseudoterminals.
See comments below.
See also pty_master_open_bsd.c.
*/
#if ! defined(__sun)
/* Prevents ptsname() declaration being visible on Solaris 8 */
#if ! defined(_XOPEN_SOURCE) || _XOPEN_SOURCE < 600
#define _XOPEN_SOURCE 600
#endif
#endif
#include "pty_master_open.h" /* Declares ptyMasterOpen() */
/* Some implementations don't have posix_openpt() */
#if defined(__sun) /* Not on Solaris 8 */
#define NO_POSIX_OPENPT
#endif
#ifdef NO_POSIX_OPENPT
static int
posix_openpt(int flags)
{
return open("/dev/ptmx", flags);
}
#endif
/* Open a pty master, returning file descriptor, or -1 on error.
On successful completion, the name of the corresponding pty
slave is returned in 'slaveName'. 'snLen' should be set to
indicate the size of the buffer pointed to by 'slaveName'. */
int ptyMasterOpen(char *slaveName, size_t snLen)
{
int masterFd, savedErrno;
char *p;
masterFd = posix_openpt(O_RDWR | O_NOCTTY); /* Open pty master */
if (masterFd == -1)
return -1;
if (grantpt(masterFd) == -1) { /* Grant access to slave pty */
savedErrno = errno;
close(masterFd); /* Might change 'errno' */
errno = savedErrno;
return -1;
}
if (unlockpt(masterFd) == -1) { /* Unlock slave pty */
savedErrno = errno;
close(masterFd); /* Might change 'errno' */
errno = savedErrno;
return -1;
}
p = ptsname(masterFd); /* Get slave pty name */
if (p == nullptr) {
savedErrno = errno;
close(masterFd); /* Might change 'errno' */
errno = savedErrno;
return -1;
}
if (strlen(p) < snLen) {
strncpy(slaveName, p, snLen);
} else { /* Return an error if buffer too small */
close(masterFd);
errno = EOVERFLOW;
return -1;
}
return masterFd;
}

View File

@@ -0,0 +1,32 @@
#ifndef LCPEX_PTY_MASTER_OPEN_H
#define LCPEX_PTY_MASTER_OPEN_H
#include <sys/types.h>
#include <errno.h>
#include <cstdlib>
#include <fcntl.h>
#include <csignal>
#include <cstring>
#include <stdlib.h>
#include <fcntl.h>
/**
* @brief Open a pseudo-terminal master
*
* This function opens a pseudo-terminal master and returns the file descriptor
* for the master. The name of the corresponding pseudo-terminal slave is also
* returned in the buffer pointed to by 'slaveName'.
*
* @param[out] slaveName Buffer to store the name of the slave pty
* @param[in] snLen Length of the 'slaveName' buffer
*
* @return File descriptor of the master pty on success, or -1 on error
*
* On success, the name of the corresponding pseudo-terminal slave is stored in
* the buffer pointed to by 'slaveName'. If the buffer is too small to hold the
* name of the slave, an error of type 'EOVERFLOW' is returned.
*/
int ptyMasterOpen(char *slaveName, size_t snLen);
#endif //LCPEX_PTY_MASTER_OPEN_H

View File

@@ -0,0 +1,77 @@
#include "tty_functions.h"
/* Reset terminal mode on program exit */
void ttyResetExit( struct termios * ttyOrig )
{
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, ttyOrig) == -1)
perror("tcsetattr");
}
/* Place terminal referred to by 'fd' in cbreak mode (noncanonical mode
with echoing turned off). This function assumes that the terminal is
currently in cooked mode (i.e., we shouldn't call it if the terminal
is currently in raw mode, since it does not undo all of the changes
made by the ttySetRaw() function below). Return 0 on success, or -1
on error. If 'prevTermios' is non-NULL, then use the buffer to which
it points to return the previous terminal settings. */
int ttySetCbreak(int fd, struct termios *prevTermios)
{
struct termios t;
if (tcgetattr(fd, &t) == -1)
return -1;
if (prevTermios != nullptr)
*prevTermios = t;
t.c_lflag &= ~(ICANON | ECHO);
t.c_lflag |= ISIG;
t.c_iflag &= ~ICRNL;
t.c_cc[VMIN] = 1; /* Character-at-a-time input */
t.c_cc[VTIME] = 0; /* with blocking */
if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
return -1;
return 0;
}
/* Place terminal referred to by 'fd' in raw mode (noncanonical mode
with all input and output processing disabled). Return 0 on success,
or -1 on error. If 'prevTermios' is non-NULL, then use the buffer to
which it points to return the previous terminal settings. */
int ttySetRaw(int fd, struct termios *prevTermios)
{
struct termios t;
if (tcgetattr(fd, &t) == -1)
return -1;
if (prevTermios != nullptr)
*prevTermios = t;
t.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
/* Noncanonical mode, disable signals, extended
input processing, and echoing */
t.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR |
INPCK | ISTRIP | IXON | PARMRK);
/* Disable special handling of CR, NL, and BREAK.
No 8th-bit stripping or parity error handling.
Disable START/STOP output flow control. */
// this breaks cursor position change tracking in parent processes
//t.c_oflag &= ~OPOST; /* Disable all output processing */
t.c_cc[VMIN] = 1; /* Character-at-a-time input */
t.c_cc[VTIME] = 0; /* with blocking */
if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
return -1;
return 0;
}

View File

@@ -0,0 +1,59 @@
#ifndef LCPEX_TTY_FUNCTIONS_H
#define LCPEX_TTY_FUNCTIONS_H
#include <termios.h>
#include <cstdio>
#include <iostream>
#include <unistd.h>
/**
* @brief Place terminal referred to by 'fd' in cbreak mode
*
* This function places the terminal referred to by the file descriptor 'fd'
* in cbreak mode (noncanonical mode with echoing turned off). It assumes that
* the terminal is currently in cooked mode (i.e., it should not be called
* if the terminal is currently in raw mode, since it does not undo all of
* the changes made by the ttySetRaw() function).
*
* @param fd File descriptor of the terminal
* @param prevTermios Buffer to store the previous terminal settings
*
* @return 0 on success, or -1 on error
*
* If 'prevTermios' is non-NULL, then the buffer pointed to by it will be used
* to return the previous terminal settings.
*/
int ttySetCbreak(int fd, struct termios * prevTermios);
/**
* @brief Place terminal referred to by 'fd' in raw mode
*
* This function places the terminal referred to by the file descriptor 'fd'
* in raw mode.
*
* @param fd File descriptor of the terminal
* @param prevTermios Buffer to store the previous terminal settings
*
* @return 0 on success, or -1 on error
*
* If 'prevTermios' is non-NULL, then the buffer pointed to by it will be used
* to return the previous terminal settings.
*/
int ttySetRaw(int fd, struct termios * prevTermios);
/**
* @brief Reset terminal mode on program exit
*
* @param ttyOrig Original terminal mode to be restored
*
* This function resets the terminal mode when the program exits by using the
* tcsetattr function. If the tcsetattr function returns an error, the perror
* function is called to print an error message.
*/
void ttyResetExit( struct termios * ttyOrig );
#endif //LCPEX_TTY_FUNCTIONS_H