aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sshexec.c488
1 files changed, 342 insertions, 146 deletions
diff --git a/sshexec.c b/sshexec.c
index aca9ec9..4074795 100644
--- a/sshexec.c
+++ b/sshexec.c
@@ -9,13 +9,29 @@
#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)
{
@@ -27,8 +43,19 @@ usage(void)
}
+/**
+ * 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;
};
@@ -52,55 +79,143 @@ static const enum optclass {
#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)
{
size_t n = 0;
+
+ /* Quote empty string */
if (!*arg) {
build_command_asis("''");
return;
}
+
+ /* If the string only contains safe characters, add it would escaping */
while (isalnum(arg[n]) || arg[n] == '_' || 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;
while (*arg) {
@@ -125,39 +240,124 @@ build_command_escape(const char *arg)
}
}
build_command_asis("\\n')\"");
+ /* TODO sh(1) may remove all, not just the last LF */
}
-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;
- const char *cd = NULL;
- const char *asis = NULL;
- const char *nasis_str = NULL;
- struct redirection *redirections = NULL;
- size_t nredirections = 0;
- char *destination;
- char *dest_dir_delim;
- char **opts, *p;
- size_t nopts;
- enum optclass class;
- const char *arg;
- char opt;
- const char **args;
+ int next_asis = 0;
size_t i;
- int strict_cd;
- unsigned long long int nasis = ULLONG_MAX;
- int next_asis;
- (void) argc_unused;
+ /* Change directory */
+ if (dir) {
+ build_command_asis("cd -- ");
+ build_command_escape(dir);
+ build_command_asis(strict_cd ? " && " : " ; ");
+ }
- if (*argv)
- argv0 = *argv++;
+ /* 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(" )");
- p = strrchr(argv0, '/');
- is_sshcd = !strcmp(p ? &p[1] : argv0, "sshcd");
+ /* 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;
+ }
+}
+
+
+/**
+ * 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))) {\
@@ -166,59 +366,66 @@ main(int argc_unused, char *argv[])
continue;\
}
- if (*argv && !strcmp(*argv, "{")) {
- argv++;
- for (; *argv && strcmp(*argv, "}"); argv++) {
- STORE_OPT(&ssh, "ssh")
- STORE_OPT(&cd, "cd")
+ /* Get options */
+ for (; *argv && strcmp(*argv, "}"); argv++) {
+ /* Options recognised in both sshexec(1) and sshcd(1) */
+ STORE_OPT(&ssh, "ssh")
+ STORE_OPT(&cd, "cd")
- if (is_sshcd)
- usage();
-
- STORE_OPT(&dir, "dir")
- STORE_OPT(&asis, "asis")
- STORE_OPT(&nasis_str, "nasis")
+ /* The rest of the options only recognised in sshexec(1) */
+ if (is_sshcd)
+ usage();
- p = *argv;
- while (isdigit(*p))
+ /* 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++;
- 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 == '-') {
+ while (isdigit(*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
+end_of_options:
+ /* Parse options and set defaults */
+
if (!ssh)
ssh = "ssh";
@@ -241,8 +448,30 @@ main(int argc_unused, char *argv[])
usage();
}
- opts = argv;
- nopts = 0;
+ 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
+ * @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)
+{
+ enum optclass class;
+ const char *arg;
+ char opt;
+ size_t nopts = 0;
+
while (*argv) {
if (!strcmp(*argv, "--")) {
argv++;
@@ -271,79 +500,47 @@ main(int argc_unused, char *argv[])
}
}
+ *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);
- 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 (dir) {
- build_command_asis("cd -- ");
- build_command_escape(dir);
- build_command_asis(strict_cd ? " && " : " ; ");
- }
- if (is_sshcd) {
- build_command_asis("exec \"$SHELL\" -l");
- goto command_constructed;
- }
- if (asis)
- build_command_asis("( env --");
- else
- build_command_asis("exec env --");
- next_asis = 0;
- 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(" )");
- 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);
- }
- }
-command_constructed:
- finalise_command();
+ /* 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)
@@ -362,7 +559,6 @@ command_constructed:
args[i++] = destination;
args[i++] = command;
args[i++] = NULL;
-
#if defined(__GNUC__)
# pragma GCC diagnostic ignored "-Wcast-qual"
#endif