aboutsummaryrefslogblamecommitdiffstats
path: root/sshexec.c
blob: 7f495302332eab253b07d959ed10700935b36382 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11










                                                         


                          
                                     




                                   
                        

 




                                                   


                                                            


                                


           




                                                                                                     

 
 


                              
                    


                                                                     
                         




                                                                   



                           


                                       
                            



                                             
                               



                                           


                              

















































                                                          

                                          
                                                            


                                    




                                                          
                              
                                   
 


                               



                            






                                                                      


                               










                                                                       


 




                                            


                                     


                                                                      
                     
                       

                                



                                         

                                                                                
                                      




                                      

                                                             
                                           
                                                                                                    
                      















                                                                         
 












                                                                                      
                      
                                                                                 

                               
                                                      






                                              










                                                         


 






                                                                                      
 
                          

                 





                                                               
 
























                                                            
 



















































                                                                                                                  






































                                                                                     

















                                                                 
 





                                                                    
 




                                                                        
 


                                                                           
 























                                                                                                   
                                    
                                                   
                                            

                                               
                         





                                                                  
                 
                                                           
         
 




                                                                                       

                


                                            




                                             
 


                                       





                                    









                                                      











                                                                  

                                                                  





                                                                     






                                      
                        

                         









                                                                             
                       
                                                   





                                                              

                              
                        















                                                                                                  

                                                





                                                                                  
                                 
                              

                                                                
                                                                            
                                                                     









                                                                                      
                                                                                                           
                                         
                                                                          
                                                                                                                          
                                              
                                                                        
                                                                                              
                                                   


                                                                                                                    
                                                                             







                                                                                                                       
                                        
                                                                                           
                                 



                         

























                                                                                               
                              
                                                          

                        
                                    
                                      
                                                                             
                                          

                                                                                 


                                             
 
                                                        
              
                                                                    


                                                                       
                       

                                                                               

                                                   





                                                      





                                                                             
/* See LICENSE file for copyright and license details. */
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>


/**
 * The name of the process
 */
static const char *argv0 = "sshexec";

/**
 * Whether the process is sshcd(1),
 * otherwise it's sshexec(1)
 */
static int is_sshcd = 0;


/**
 * Print an error message and exit
 *
 * @param  ...  Message format string and arguments
 */
#define exitf(...) (fprintf(stderr, __VA_ARGS__), exit(255))


/**
 * Print usage synopsis and exit
 */
static void
usage(void)
{
	exitf("usage: %s [{ [ssh=command]%s [cd=(strict|lax)]%s }] [ssh-option] ... destination%s\n",
	      argv0,
	      is_sshcd ? "" : " [dir=directory]",
	      is_sshcd ? "" : " [[fd]{>,>>,>|,<>}[&]=file] [asis=asis-marker [nasis=asis-count]]",
	      is_sshcd ? "" : " command [argument] ...");
}


/**
 * File descriptor redirection
 */
struct redirection {
	/**
	 * First part of the redirection string, shall not be escaped
	 */
	const char *asis;

	/**
	 * Second part of the redirection string, shall be escaped;
	 * or `NULL` if the entire string is contained in `.asis`
	 */
	const char *escape;
};


/**
 * Command being constructor for ssh(1)
 */
static char *command = NULL;

/**
 * The number of bytes allocated to `command`
 */
static size_t command_size = 0;

/**
 * The number of bytes written to `command`
 */
static size_t command_len = 0;


/**
 * The directory to use as the remote working directory
 */
static const char *dir = NULL;

/**
 * The ssh command to use
 */
static const char *ssh = NULL;

/**
 * Whether the command shall fail on failure to
 * set remote working directory
 */
static int strict_cd;

/**
 * The string used to mark the next argument
 * for unescaped inclusion in the command string
 */
static const char *asis = NULL;

/**
 * The number of times `asis` may be recognised
 */
static unsigned long long int nasis = ULLONG_MAX;

/**
 * File redirections to apply after directory switch
 */
static struct redirection *redirections = NULL;

/**
 * The number of elements in `redirections`
 */
static size_t nredirections = 0;

/**
 * The destination command line operand
 */
static char *destination;


/**
 * Add text to `command`
 *
 * @param  TEXT:const char *  The text to add to `command`
 * @param  N:size_t           The number of bytes to write
 */
#define build_command(TEXT, N)\
	do {\
		build_command_reserve(N);\
		memcpy(&command[command_len], (TEXT), (N));\
		command_len += (N);\
	} while (0)

/**
 * Add text to `command`
 *
 * @param  TEXT:const char *  The text to add to `command`
 */
#define build_command_asis(S)\
	build_command(S, strlen(S))

/**
 * NUL-byte terminate `command`
 */
#define finalise_command()\
	build_command("", 1)


/**
 * Allocate space for `command`
 * 
 * @param  n  The number of additional bytes (from what has currently
 *            been written, rather than currently allocated) `command`
 *            should have room for
 */
static void
build_command_reserve(size_t n)
{
	if (n <= command_size - command_len)
		return;

	if (n < 512)
		n = 512;
	if (command_len + n > SIZE_MAX)
		exitf("%s: could not allocate enough memory\n", argv0);
	command_size = command_len + n;
	command = realloc(command, command_size);
	if (!command)
		exitf("%s: could not allocate enough memory\n", argv0);
}


/**
 * Escape a string and add it to `command`
 *
 * @param  arg  The string to escape and add
 */
static void
build_command_escape(const char *arg)
{
#define IS_ALWAYS_SAFE(C)   (isalnum((C)) || (C) == '_' || (C) == '/')
#define IS_INITIAL_SAFE(C)  (isalpha((C)) || (C) == '_' || (C) == '/')

	size_t n = 0;
	size_t lfs = 0;

	/* Quote empty string */
	if (!*arg) {
		build_command_asis("''");
		return;
	}

	/* If the string only contains safe characters, add it would escaping */
	while (IS_ALWAYS_SAFE(arg[n]))
		n += 1;
	if (!arg[n]) {
		build_command(arg, n);
		return;
	}

	/* Escape string, using quoted printf(1) statement */
	build_command_asis("\"$(printf '");
	goto start; /* already have a count of safe initial characters, let's add them immidately */
	while (*arg) {
		/* Since process substation removes at least one terminal
		 * LF, we must hold of on adding them */
		if (*arg == '\n') {
			lfs += 1;
			arg = &arg[1];
			continue;
		}

		/* Adding any held back LF */
		if (lfs) {
			build_command_reserve(lfs * 2U);
			for (; lfs; lfs--) {
				command[command_len++] = '\\';
				command[command_len++] = 'n';
			}
		}

		/* Character is unsafe, escape it */
		if (!IS_ALWAYS_SAFE(*arg)) {
			build_command_reserve(4);
			command[command_len++] = '\\';
			command[command_len] = (((unsigned char)*arg >> 6) & 7) + '0';
			command_len += (command[command_len] != '0');
			command[command_len] = (((unsigned char)*arg >> 3) & 7) + '0';
			command_len += (command[command_len] != '0');
			command[command_len++] = ((unsigned char)*arg & 7) + '0';
			arg = &arg[1];
		}

		/* Add any safe characters as is */
		n = 0;
		while (IS_INITIAL_SAFE(arg[n]) || arg[n] == '_' || arg[n] == '/')
			n += 1;
		if (n) {
			while (IS_ALWAYS_SAFE(arg[n]))
				n += 1;
		start:
			build_command(arg, n);
			arg = &arg[n];
		}
	}
	build_command_asis("\\n')\"");
	/* Add any held back terminal LF's */
	if (lfs) {
		build_command_reserve(lfs + 2U);
		command[command_len++] = '\'';
		memset(&command[command_len], '\n', lfs);
		command_len += lfs;
		command[command_len++] = '\'';
	}

#undef IS_ALWAYS_SAFE
#undef IS_INITIAL_SAFE
}


/**
 * Construct `command`
 *
 * @param  argv  The command to execute remotely and its arguments (`NULL` terminated)
 */
static void
construct_command(char *argv[])
{
	int next_asis = 0;
	size_t i;

	/* Change directory */
	if (dir) {
		build_command_asis("cd -- ");
		build_command_escape(dir);
		build_command_asis(strict_cd ? " && " : " ; ");
	}

	/* Execute command */
	if (is_sshcd) {
		build_command_asis("exec \"$SHELL\" -l");
		goto command_constructed;
	}
	if (asis)
		build_command_asis("( env --");
	else
		build_command_asis("exec env --");
	for (; *argv; argv++) {
		if (asis && nasis && !strcmp(*argv, asis)) {
			nasis -= 1U;
			next_asis = 1;
		} else {
			build_command_asis(" ");
			if (next_asis) {
				next_asis = 0;
				build_command_asis(*argv);
			} else {
				build_command_escape(*argv);
			}
		}
	}
	if (asis)
		build_command_asis(" )");

	/* Set redirections */
	for (i = 0; i < nredirections; i++) {
		build_command_asis(" ");
		build_command_asis(redirections[i].asis);
		if (redirections[i].escape) {
			build_command_asis(" ");
			build_command_escape(redirections[i].escape);
		}
	}

	/* NUL-terminate `command` */
command_constructed:
	finalise_command();
}


/**
 * Replace "sshexec://" prefix in `destination`
 * with "ssh://" and remove the directory
 * component and, if it present, store the
 * directory to `directory`
 */
static void
extract_directory_from_destination(void)
{
	char *dest_dir_delim;
	if (!is_sshcd && !strncmp(destination, "sshexec://", sizeof("sshexec://") - 1U)) {
		memmove(&destination[sizeof("ssh") - 1U],
		        &destination[sizeof("sshexec") - 1U],
		        strlen(&destination[sizeof("sshexec") - 1U]) + 1U);
		goto using_ssh_prefix;
	} else if (is_sshcd && !strncmp(destination, "sshcd://", sizeof("sshcd://") - 1U)) {
		memmove(&destination[sizeof("ssh") - 1U],
		        &destination[sizeof("sshcd") - 1U],
		        strlen(&destination[sizeof("sshcd") - 1U]) + 1U);
		goto using_ssh_prefix;
	} else if (!strncmp(destination, "ssh://", sizeof("ssh://") - 1U)) {
	using_ssh_prefix:
		dest_dir_delim = strchr(&destination[sizeof("ssh://")], '/');
	} else {
		dest_dir_delim = strchr(destination, ':');
	}
	if (dest_dir_delim) {
		if (dir)
			exitf("%s: directory specified both in 'dir' option and in destination operand\n", argv0);
		*dest_dir_delim++ = '\0';
		dir = dest_dir_delim;
	}
}


/**
 * Get an environment variable or a default value
 * 
 * @param   var  The name of the environment variable
 * @param   def  The default value to return if the environment variable is not set
 * @return       The value of the environment variable, or the default value if unset
 */
static const char *
get(const char *var, const char *def)
{
	const char *ret = getenv(var);
	return ret ? ret : def;
}


/**
 * Check if a string appears as a word in another string
 * 
 * @param   set     The string to search in; words are separated by spaces
 * @param   sought  The string to search for
 * @return          1 if the string is found, 0 otherwise
 */
#if defined(__GNUC__)
__attribute__((__pure__))
#endif
static int
contains(const char *set, const char *sought)
{
	size_t m, n = strlen(sought);
	const char *p, *q;
	for (p = set; (q = strchr(p, ' ')); p = &q[1]) {
		m = (size_t)(q - p);
		if (m == n && !strncmp(p, sought, n))
			return 1;
	}
	return !strcmp(p, sought);
}


/**
 * Read and parse the sshexec options
 *
 * @param   argv  The arguments from the command line after
 *                the zeroth argument
 * @return        `argv` offset to skip pass any sshexec options
 */
static char **
parse_sshexec_options(char *argv[])
{
	const char *cd = NULL;
	const char *nasis_str = NULL;
	char *p;

	/* Check that we have any arguments to parse, and that we
	 * have the "{" mark the beginning of sshexec options. */
	if (!*argv || strcmp(*argv, "{"))
		goto end_of_options;
	argv++;

#define STORE_OPT(VARP, OPT)\
	if (!strncmp(*argv, OPT"=", sizeof(OPT))) {\
		if (*(VARP) || !*(*(VARP) = &(*argv)[sizeof(OPT)]))\
			usage();\
		continue;\
	}

	/* Get options */
	for (; *argv && strcmp(*argv, "}"); argv++) {
		/* Options recognised in both sshexec(1) and sshcd(1) */
		STORE_OPT(&ssh, "ssh")
		STORE_OPT(&cd, "cd")

		/* The rest of the options only recognised in sshexec(1) */
		if (is_sshcd)
			usage();

		/* Normal options */
		STORE_OPT(&dir, "dir")
		STORE_OPT(&asis, "asis")
		STORE_OPT(&nasis_str, "nasis")

		/* File descriptor redirections */
		p = *argv;
		while (isdigit(*p))
			p++;
		if (p[0] == '>')
			p = &p[1 + (p[1] == '>' || p[1] == '|')];
		else if (p[0] == '<')
			p = &p[1 + (p[1] == '>')];
		else
			usage();
		if (p[p[0] == '&'] != '=')
			usage(); 
		redirections = realloc(redirections, (nredirections + 1U) * sizeof(*redirections));
		if (!redirections)
			exitf("%s: could not allocate enough memory\n", argv0);
		if (*p == '&') {
			p = &p[1];
			memmove(p, &p[1], strlen(&p[1]) + 1U);
			if (isdigit(*p)) {
				p++;
				while (isdigit(*p))
					p++;
			} else if (*p == '-') {
				p++;
			}
			if (*p)
				usage();
			redirections[nredirections].escape = NULL;
		} else {
			*p++ = '\0';
			redirections[nredirections].escape = p;
		}
		redirections[nredirections++].asis = *argv;
	}

	/* Check that we have the "}" argument that marks the end of sshexec options */
	if (!*argv)
		usage();
	argv++;

#undef STORE_OPT

end_of_options:
	/* Parse options and set defaults */

	if (!ssh) {
		ssh = get("SSHEXEC_SSH", "");
		if (!*ssh)
			ssh = "ssh";
	}

	if (!cd)
		strict_cd = !is_sshcd;
	else if (!strcmp(cd, "strict"))
		strict_cd = 1;
	else if (!strcmp(cd, "lax"))
		strict_cd = 0;
	else
		usage();

	if (nasis_str) {
		char *end;
		if (!asis || !isdigit(*nasis_str))
			usage();
		errno = 0;
		nasis = strtoull(nasis_str, &end, 10);
		if ((!nasis && errno) || *end)
			usage();
	}

	return argv;
}


/**
 * Read and parse the ssh(1) options
 *
 * @param   argv       The return value of `parse_sshexec_options`
 * @param   nopts_out  Output parameter for the number of option
 *                     arguments, this includes all arguments the
 *                     return value offsets `argv` except the "--"
 *                     (if there is one), so this includes both
 *                     the options themselves and their arguments,
 *                     and joined options countas one
 * @return             `argv` offset to skip pass any ssh(1) options,
 *                     and any "--" immediately after them
 */
static char **
parse_ssh_options(char *argv[], size_t *nopts_out)
{
	const char *unarged_opts;
	const char *arged_opts;
	const char *optatarged_opts;
	const char *optarged_opts;
	const char *unarged_longopts;
	const char *arged_longopts;
	const char *optarged_longopts;
	const char *arg;
	size_t nopts = 0;

	/* Get option syntax from the environment */
	unarged_opts = get("SSHEXEC_OPTS_NO_ARG", "46AaCfGgKkMNnqsTtVvXxYy");
	arged_opts = get("SSHEXEC_OPTS_ARG", "BbcDEeFIiJLlmOoPpQRSWw");
	optatarged_opts = get("SSHEXEC_OPTS_OPT_ATTACHED_ARG", "");
	optarged_opts = get("SSHEXEC_OPTS_OPT_ARG", "");
	unarged_longopts = get("SSHEXEC_LONG_OPTS_NO_ARG", "");
	arged_longopts = get("SSHEXEC_LONG_OPTS_ARG", "");
	optarged_longopts = get("SSHEXEC_LONG_OPTS_OPT_ARG", "");

	/* Count option arguments from the command line */
	while (*argv) {
		/* Break at the first non-option */
		if (!strcmp(*argv, "--")) {
			argv++;
			break;
		} else if ((*argv)[0] != '-' || !(*argv)[1]) {
			break;
		}

		arg = *argv++;
		nopts++;
		if (arg[1] == '-') {
			/* Long option */
			if (strchr(arg, '=')) {
				/* Option has attach argument */
			} else if (contains(unarged_longopts, arg)) {
				/* Option cannot have an argument */
			} else if (contains(arged_longopts, arg)) {
				/* Option has detached argument  */
				if (!*argv)
					exitf("%s: argument missing for option %s\n", argv0, arg);
				argv++;
				nopts++;
			} else if (contains(optarged_longopts, arg)) {
				/* Long option with either detached argument or no argument */
				if (*argv && **argv != '-') {
					/* The option has a detached argument */
					argv++;
					nopts++;
				}
			} else {
				exitf("%s: unrecognised option %s\n", argv0, arg);
			}
		} else {
			/* Short option */
			char opt;
			arg++;
			while ((opt = *arg++)) {
				if (strchr(unarged_opts, opt)) {
					/* Option cannot have an argument */
				} else if (strchr(arged_opts, opt)) {
					/* Option must have an argument */
					if (arg[1]) {
						/* Argument is attached to option */
						break;
					} else if (argv[1]) {
						/* Argument is detached from option */
						argv++;
						nopts++;
						break;
					} else {
						exitf("%s: argument missing for option -%c\n", argv0, opt);
					}
				} else if (strchr(optatarged_opts, opt)) {
					/* Option may have an attached argument, but it cannot have a detached argument */
					break;
				} else if (strchr(optarged_opts, opt)) {
					/* Option may have an attached or detached argument */
					if (*arg) {
						/* Argument is attached to option */
					} else {
						/* Either there is no argument, or it is detached from the option */
						if (*argv && **argv != '-') {
							/* Argument exist and is detached. We assume that if the next
							 * argument in the command line is the option's argument unless
							 * it starts with '-'. */
							argv++;
							nopts++;
							break;
						}
					}
				} else {
					exitf("%s: unrecognised option -%c\n", argv0, opt);
				}
			}
		}
	}

	*nopts_out = nopts;
	return argv;
}


int
main(int argc_unused, char *argv[])
{
	char **opts, *p;
	size_t nopts;
	const char **args;
	size_t i;

	(void) argc_unused;

	/* Identify used command name */
	if (*argv)
		argv0 = *argv++;
	p = strrchr(argv0, '/');
	is_sshcd = !strcmp(p ? &p[1] : argv0, "sshcd");

	/* Parse options */
	argv = parse_sshexec_options(argv);
	argv = parse_ssh_options(opts = argv, &nopts);

	/* Check operand count and separate out `destination` from the command and arguments */
	destination = *argv++;
	if (!destination || (is_sshcd ? !!*argv : !*argv))
		usage();

	/* Validate `destination` */
	if (!strcmp(destination, "-"))
		exitf("%s: the command argument must not be \"-\"\n", argv0);
	else if (strchr(destination, '='))
		exitf("%s: the command argument must contain an \'=\'\n", argv0);

	/* Parse command line operands */
	extract_directory_from_destination();
	construct_command(argv);

	/* Construct arguments for ssh(1) and execute */
	i = 0;
	args = calloc(5U + (size_t)is_sshcd + nopts, sizeof(*args));
	if (!args)
		exitf("%s: could not allocate enough memory\n", argv0);
	args[i++] = ssh;
	if (is_sshcd) {
		const char *pty_alloc_flag = get("SSHCD_PTY_ALLOC_FLAG", "-t");
		if (*pty_alloc_flag)
			args[i++] = pty_alloc_flag;
	}
	memcpy(&args[i], opts, nopts * sizeof(*opts));
	i += nopts;
	args[i++] = "--";
	args[i++] = destination;
	args[i++] = command;
	args[i++] = NULL;
#if defined(__GNUC__)
# pragma GCC diagnostic ignored "-Wcast-qual"
#endif
	execvp(ssh, (char **)args);
	exitf("%s: failed to execute %s: %s\n", argv0, ssh, strerror(errno));
}