aboutsummaryrefslogtreecommitdiffstats
path: root/sshexec.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--sshexec.c683
1 files changed, 548 insertions, 135 deletions
diff --git a/sshexec.c b/sshexec.c
index 1ad2cf8..7f49530 100644
--- a/sshexec.c
+++ b/sshexec.c
@@ -9,111 +9,235 @@
#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 [{ %s }] [ssh-option] ... destination command [argument] ...\n",
- argv0, "[ssh=command] [dir=directory] [[fd]{>,>>,>|,<>}[&]=file]");
+ 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;
};
-static const enum optclass {
- NO_RECOGNISED = 0,
- NO_ARGUMENT,
- MANDATORY_ARGUMENT
-} sshopts[(size_t)1 << CHAR_BIT] = {
-#define X(F) [F] = NO_ARGUMENT
- X('4'), X('6'), X('A'), X('a'), X('C'), X('f'), X('G'),
- X('g'), X('K'), X('k'), X('M'), X('N'), X('n'), X('q'),
- X('s'), X('T'), X('t'), X('V'), X('v'), X('X'), X('x'),
- X('Y'), X('y'),
-#undef X
-#define X(F) [F] = MANDATORY_ARGUMENT
- X('B'), X('b'), X('c'), X('D'), X('E'), X('e'), X('F'),
- X('I'), X('i'), X('J'), X('L'), X('l'), X('m'), X('O'),
- X('o'), X('P'), X('p'), X('Q'), X('R'), X('S'), X('W'),
- X('w')
-#undef X
-};
-
+/**
+ * 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;
-#define build_command(DATA, N)\
+/**
+ * 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], (DATA), (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) {
- 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);
- }
+ 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;
}
- while (isalnum(arg[n]) || arg[n] == '_' || arg[n] == '/')
+
+ /* 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;
+ goto start; /* already have a count of safe initial characters, let's add them immidately */
while (*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];
+ /* 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 (isalpha(arg[n]) || arg[n] == '_' || arg[n] == '/')
+ while (IS_INITIAL_SAFE(arg[n]) || arg[n] == '_' || arg[n] == '/')
n += 1;
if (n) {
- while (isalnum(arg[n]) || arg[n] == '_' || arg[n] == '/')
+ while (IS_ALWAYS_SAFE(arg[n]))
n += 1;
start:
build_command(arg, n);
@@ -121,29 +245,173 @@ build_command_escape(const char *arg)
}
}
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
}
-int
-main(int argc_unused, char *argv[])
+/**
+ * Construct `command`
+ *
+ * @param argv The command to execute remotely and its arguments (`NULL` terminated)
+ */
+static void
+construct_command(char *argv[])
{
- const char *dir = NULL;
- const char *ssh = NULL;
- struct redirection *redirections = NULL;
- size_t nredirections = 0;
- const char *destination;
- char **opts, *p;
- size_t nopts;
- enum optclass class;
- const char *arg;
- char opt;
- const char **args;
+ int next_asis = 0;
size_t i;
- (void) argc_unused;
+ /* 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;
+ }
+}
- if (*argv)
- argv0 = *argv++;
+
+/**
+ * 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))) {\
@@ -152,121 +420,266 @@ main(int argc_unused, char *argv[])
continue;\
}
- if (*argv && !strcmp(*argv, "{")) {
- argv++;
- for (; *argv && strcmp(*argv, "}"); argv++) {
- STORE_OPT(&ssh, "ssh")
- STORE_OPT(&dir, "dir")
+ /* 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")
- p = *argv;
- while (isdigit(*p))
+ /* 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++;
- 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)) {
+ while (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;
+ } else if (*p == '-') {
+ p++;
}
- redirections[nredirections++].asis = *argv;
+ if (*p)
+ usage();
+ redirections[nredirections].escape = NULL;
+ } else {
+ *p++ = '\0';
+ redirections[nredirections].escape = p;
}
- if (!*argv)
- usage();
- argv++;
+ redirections[nredirections++].asis = *argv;
}
+ /* Check that we have the "}" argument that marks the end of sshexec options */
+ if (!*argv)
+ usage();
+ argv++;
+
#undef STORE_OPT
- if (!ssh)
- ssh = "ssh";
+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", "");
- opts = argv;
- nopts = 0;
+ /* 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++)[1];
+
+ arg = *argv++;
nopts++;
- while (*arg) {
- opt = *arg++;
- class = sshopts[(unsigned char)opt];
- if (class == MANDATORY_ARGUMENT) {
- if (*arg) {
- break;
- } else if (*argv) {
+ 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: argument missing for option -%c\n", argv0, opt);
+ exitf("%s: unrecognised option -%c\n", argv0, opt);
}
- } else if (class == NO_RECOGNISED) {
- 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 && !*argv)
+ if (!destination || (is_sshcd ? !!*argv : !*argv))
usage();
- if (dir) {
- build_command_asis("cd -- ");
- build_command_escape(dir);
- build_command_asis(" && ");
- }
- build_command_asis("exec --");
- for (; *argv; argv++) {
- build_command_asis(" ");
- build_command_escape(*argv);
- }
- 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);
- }
- }
- finalise_command();
+ /* 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(5 + nopts, sizeof(*args));
+ 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