diff options
Diffstat (limited to '')
-rw-r--r-- | sshexec.c | 683 |
1 files changed, 548 insertions, 135 deletions
@@ -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 |