diff options
Diffstat (limited to '')
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | README | 203 | ||||
-rw-r--r-- | sshcd.1 | 361 | ||||
-rw-r--r-- | sshexec.1 | 249 | ||||
-rw-r--r-- | sshexec.c | 683 |
6 files changed, 1338 insertions, 165 deletions
@@ -1,6 +1,6 @@ ISC License -© 2023 Mattias Andrée <maandree@kth.se> +© 2023, 2024, 2025 Mattias Andrée <m@maandree.se> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -21,11 +21,16 @@ install: sshexec mkdir -p -- "$(DESTDIR)$(PREFIX)/bin" mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" cp -- sshexec "$(DESTDIR)$(PREFIX)/bin/" + test ! -d "$(DESTDIR)$(PREFIX)/bin/sshcd" + ln -sf -- sshexec "$(DESTDIR)$(PREFIX)/bin/sshcd" cp -- sshexec.1 "$(DESTDIR)$(MANPREFIX)/man1/" + cp -- sshcd.1 "$(DESTDIR)$(MANPREFIX)/man1/" uninstall: -rm -f -- "$(DESTDIR)$(PREFIX)/bin/sshexec" + -rm -f -- "$(DESTDIR)$(PREFIX)/bin/sshcd" -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/sshexec.1" + -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/sshcd.1" clean: -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda @@ -2,20 +2,25 @@ NAME sshexec - run a command through ssh(1) with normal command syntax SYNOPSIS - sshexec [{ [ssh=ssh-command] [dir=directory] }] [ssh-option] ... - destination command [argument] ... + sshexec [{ [ssh=ssh-command] [dir=directory] [cd=(strict|lax)] + [[fd]{>,>>,>|,<,<>}[&]=file] [asis=asis-marker + [nasis=asis-count]] }] [ssh-option] ... destination + command [argument] ... DESCRIPTION The sshexec utility is a wrapper for SSH that makes it easy to run commands directly in the SSH command. sshexec passes any argument after } to ssh-command (ssh if not - specified), and only modifies command [argument] ... and inserts - extra arguments after destination (it may also add a -- argument - immediately before destination) to cause the remote shell it - change working directory to directory, if specified, and execute - the provided command and arguments as a regular command rather - than as shell code joined by together by spaces. + specified), but it rewrites command and the arguments to one + argument that can be passed into ssh(1) to describe each argument + as separate arguments. It may also rewrite destination to remove + information that's not supported by ssh(1) and inserts extra + arguments after it (it may also add a -- argument immediately + before destination) to cause the remote shell it change working + directory to directory, if specified, and execute the provided + command and arguments as a regular command rather than as shell + code joined by together by spaces. OPTIONS sshexec options may be placed at the very beginning enclosed @@ -39,14 +44,98 @@ OPTIONS In the remote, change working directory to directory before executing command. + cd=strict + Fail without executing the command if it's not possible + to set directory as the remote working directory. + + cd=lax + Continue (but warn) executing the command even if it's + not possible to set directory as the remote working + directory. + + [fd]>=file + After changing working directory (assuming one is + specified), create or truncate the specified file and + open it for writing, using file descriptor number fd. + (Default fd is 1 (standard output).) + + [fd]>>=file + After changing working directory (assuming one is + specified), create the specified file if it does not + exist and open it for writing in append-mode, using + file descriptor number fd. (Default fd is 1 + (standard output).) + + [fd]>|=file + After changing working directory (assuming one is + specified), create the specified file, but fail if it + already exists, and open it for writing, using file + descriptor number fd. (Default fd is 1 (standard + output).) + + [fd]<=file + After changing working directory (assuming one is + specified), open the specified file, for reading, using + file descriptor number fd. (Default fd is 0 (standard + input).) + + [fd]<>=file + After changing working directory (assuming one is + specified), open the specified file, for reading and + writing, creating it if it does not already exist, + using file descriptor number fd. (Default fd is 0 + (standard input).) + + [fd]{>,>>,>|}&=file + Duplicate the file descriptor fd giving the new file + descriptor the number file. (Default fd is 1 (standard + output).) + + [fd]{>,>>,>|}&=- + Close the file descriptor fd. (Default fd is 1 + (standard output).) + + [fd]{<,<>}&=file + Duplicate the file descriptor fd giving the new file + descriptor the number file. (Default fd is 0 (standard + input).) + + [fd]{<,<>}&=- + Close the file descriptor fd. (Default fd is 0 + (standard input).) + + asis=asis-marker + Any argument equal to asis-marker will be skipped over + and instead the next argument (regardless of whether + it to is equal to asis-marker) will be interpreted as + raw shell code string that shall be inserted without + escaping. + + masis=asis-count + If specified, asis-marker shall only have it's specified + affect up to asis-count times. + OPERANDS The following operands are supported: destination - This operand is passed as is (without validation) to - the ssh(1) utility. The ssh(1) utility will expect it - the be either in the form [user@]hostname or in the - form ssh://[user@]hostname[:port]. + The destination to connect and log into. It shall be + eitherin the form [user@]hostname[:directory] or in the + form ssh[exec]://[user@]hostname[:port][/directory]. + + user shall be the name of the remote user. If not + specified, the name of the local user running the + utility will be used. + + hostname shall be the address to the remote machine. + + port shall be the port or service name for the port + to connect to on the remote machine. + + directory shall be directory to change the remote + working directory. This is an alternative to (with the + exact same behaviour) to the dir option and cannot be + combined with it. command [argument] ... Whereas the ssh(1) utility would simply join the command @@ -56,5 +145,93 @@ OPERANDS of the as separate arguments and cause the shell to executing them as a non-builtin command. + command must not contain an equals sign (=) or be just + a dash ("-"). + +ENVIRONMENT VARIABLES + The following environment variables affects the execution of + sshexec: + + PATH + Default. See to the Base Definitions volume of POSIX.1-2017, + Section 8.3, Other Environment Variables. This environment + variable affects where the sshexec utility can find the + ssh(1) utility or ssh-command. + + SSHEXEC_SSH + If set and non-empty, it overrides the default value of + ssh-command from ssh to the value of the variable. + + SSHEXEC_OPTS_NO_ARG + List of options that sshexec shall interpret as ssh(1) + options that do not have any argument. (Default is + 46AaCfGgKkMNnqsTtVvXxYy, meaning the options -4, -6, -A, + -a, -C, -f, -G, -g, -K, -k, -M, -N, -n, -q, -s, -T, -t, + -V, -v, -X, -x, -Y, and -y.) + + SSHEXEC_OPTS_ARG + List of options that sshexec shall interpret as ssh(1) + options that have an argument. (Default is + BbcDEeFIiJLlmOoPpQRSWw, meaning the options -B, -b, -c, + -D, -E, -e, -F, -I, -i, -J, -L, -l, -m, -O, -o, -P, -p, + -Q, -R, -S, -W, and -w.) + + SSHEXEC_OPTS_OPT_ATTACHED_ARG + List of options that sshexec shall interpret as ssh(1) + options that have an argument only if there are + additional characters after the option character in the + same command line argument. (Default is the empty + string, meaning no options.) + + SSHEXEC_OPTS_OPT_ARG + List of options that sshexec shall interpret as ssh(1) + options that have an argument if there are additional + characters after the option character in the same + command line argument or if argument is followed + directly by another argument which does not start with + a dash (-). (Default is the empty string, meaning no + options.) + + SSHEXEC_LONG_OPTS_NO_ARG + Space-separated list of long options that sshexec shall + interpret as ssh(1) options that do not have any + argument unless it is followed directly by an equals + sign (=) in the same command line argument. Options + that do not start with two dashes (--) are silently + ignored. (Default is the empty string, meaning no + options.) + + SSHEXEC_LONG_OPTS_ARG + Space-separated list of long options that sshexec shall + interpret as ssh(1) options that have an argument that + must either be specified in the next command line + argument or after an equals sign (=) the shall directly + follow the option string in the same command line + argument. Options that do not start with two dashes + (--) are silently ignored. (Default is the empty string, + meaning no options.) + + SSHEXEC_LONG_OPTS_OPT_ARG + Space-separated list of long options that sshexec shall + interpret as ssh(1) options that have an argument if + it is the option string is is directly followed by + equals sign (=) in the same command line argument or if + argument is followed directly by another argument which + does not start with a dash (-). Options that do not + start with two dashes (--) are silently ignored. + (Default is the empty string, meaning no options.) + + Other environment variables may affect the execution of the + ssh(1) utility. + +BUGS + The remote shell must be sufficiently similar to sh(1posix). + Namely, it must support the cd builtin command and the commands + exec and printf is expected by POSIX. Additionally, it must + support "$( )", ' ', and &&, and argument separation with the + SP character. The remote shell must also not treat any + alphanumeric character, underscore (_) or slash (/) as special + characters. + SEE ALSO - ssh(1) + ssh(1), sshcd(1) @@ -0,0 +1,361 @@ +.TH SSHCD 1 sshexec + +.SH NAME +sshcd - open ssh(1) with a specific remote working directory + +.SH SYNOPSIS +.B sshcd +.RB [ { +.RI [\fBssh=\fP ssh-command ] +.RB [ cd= ( strict | lax )] +.BR } ] +.RI [ ssh-option ]\ ...\, +.I destination + +.SH DESCRIPTION +The +.B sshcd +utility is a wrapper for SSH that makes it lets the user +specify the directory remote working directory. +.PP +.B sshcd +passes any argument after +.B } +to +.I ssh-command +.RB ( ssh +if not specified), except it may rewrite +.I destination +to remove information that's not supported by +.BR ssh (1). + +.SH OPTIONS +.B sshcd +options may be placed at the very beginning enclosed with +the arguments +.B { +and +.BR } . +.B sshcd +options, if any, shall be placed in the same +.B { +.BR } -group. +Any other option will be passed as is to the +.BR ssh (1) +utility or +.IR ssh-command . +The +.B sshcd +utility has a build it list of options recognised by the +.BR ssh (1) +utility and will not allow anything matching this list. +The +.B sshcd +utility does not allow mixing options and operands: no +option may be placed after +.IR destination . +.PP +The following +.B sshcd +options are supported: +.TP +.BI ssh= ssh-command +Instead of looking for +.B ssh +in +.IR PATH , +the +.B sshcd +utility shall use +.IR ssh-command , +which it will look for in +.I PATH +if it is only a file name (does not contain a slash +.RB ( / )). +.TP +.B cd=strict +Fail without executing the +.I command +if it's not possible to set +.I directory +as the remote working directory. +.TP +.B cd=lax +Continue (but warn) executing the +.I command +even if it's not possible to set +.I directory +as the remote working directory. + +.SH OPERANDS +The following operands are supported: +.TP +.I destination +The destination to connect and log into. It shall be either in +the form +.RI [ user\fP\fB@ ] hostname [\fB:\fP directory ] +or in the form +.BR ssh [ cd ] :// [\fIuser @ ]\fIhostname\fP[ : \fIport\fP][ / \fIdirectory\fP]. + +.I user +shall be the name of the remote user. If not specified, +the name of the local user running the utility will be used. + +.I hostname +shall be the address to the remote machine. + +.I port +shall be the port or service name for the port to +connect to on the remote machine. + +.I directory +shall be directory to change the remote working directory. + +.SH STDIN +The +.B sshcd +utility itself does not use the standard input. + +.SH INPUT FILES +None. + +.SH ENVIRONMENT VARIABLES +The following environment variables affects the execution of +.BR sshcd : +.TP +.I PATH +Default. See to the Base Definitions volume of POSIX.1-2017, Section 8.3, Other Environment Variables. +This environment variable affects where the +.B sshcd +utility can find the +.BR ssh (1) +utility or +.IR ssh-command . +.TP +.I SSHCD_PTY_ALLOC_FLAG +Specifies the option to pass to +.BR ssh (1) +to tell SSH to allocate a pseudo terminal. If unset +.B -t +will be used. If set but empty, no flag will be passed to +.BR ssh (1). +.TP +.I SSHEXEC_SSH +If set and non-empty, it overrides the default value of +.I ssh-command +from +.B ssh +to the value of the variable. +.TP +.I SSHEXEC_OPTS_NO_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that do not have any argument. +(Default is +.BR 46AaCfGgKkMNnqsTtVvXxYy , +meaning the options +.BR -4 , +.BR -6 , +.BR -A , +.BR -a , +.BR -C , +.BR -f , +.BR -G , +.BR -g , +.BR -K , +.BR -k , +.BR -M , +.BR -N , +.BR -n , +.BR -q , +.BR -s , +.BR -T , +.BR -t , +.BR -V , +.BR -v , +.BR -X , +.BR -x , +.BR -Y , +and +.BR -y .) +.TP +.I SSHEXEC_OPTS_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument. +(Default is +.BR BbcDEeFIiJLlmOoPpQRSWw , +meaning the options +.BR -B , +.BR -b , +.BR -c , +.BR -D , +.BR -E , +.BR -e , +.BR -F , +.BR -I , +.BR -i , +.BR -J , +.BR -L , +.BR -l , +.BR -m , +.BR -O , +.BR -o , +.BR -P , +.BR -p , +.BR -Q , +.BR -R , +.BR -S , +.BR -W , +and +.BR -w .) +.TP +.I SSHEXEC_OPTS_OPT_ATTACHED_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument only if +there are additional characters after +the option character in the same +command line argument. (Default is +the empty string, meaning no options.) +.TP +.I SSHEXEC_OPTS_OPT_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument if there +are additional characters after +the option character in the same +command line argument or if argument is +followed directly by another argument +which does not start with a dash +.RB ( - ). +(Default is +the empty string, meaning no options.) +.TP +.I SSHEXEC_LONG_OPTS_NO_ARG +Space-separated list of long options that +.B sshexec +shall interpret as +.BR ssh (1) +options that do not have any argument +unless it is followed directly by an +equals sign +.RB ( = ) +in the same command line argument. +Options that do not start with two dashes +.RB ( -- ) +are silently ignored. (Default is the +empty string, meaning no options.) +.TP +.I SSHEXEC_LONG_OPTS_ARG +Space-separated list of long options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument that must +either be specified in the next command +line argument or after an +equals sign +.RB ( = ) +the shall directly follow the option +string in the same command line argument. +Options that do not start with two dashes +.RB ( -- ) +are silently ignored. (Default is the +empty string, meaning no options.) +.TP +.I SSHEXEC_LONG_OPTS_OPT_ARG +Space-separated list of long options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument if it is +the option string is is directly followed +by equals sign +.RB ( = ) +in the same command line argument or if +argument is followed directly by another +argument which does not start with a dash +.RB ( - ). +Options that do not start with two dashes +.RB ( -- ) +are silently ignored. (Default is the +empty string, meaning no options.) +.PP +Other environment variables may affect the execution of the +.BR ssh (1) +utility. + +.SH ASYNCHRONOUS EVENTS +Default. + +.SH STDOUT +The +.B sshcd +utility itself does not use the standard output. + +.SH STDERR +The standard error is used for diagnostic messages in the +.B sshcd +utility itself. + +.SH OUTPUT FILES +None. + +.SH EXTENDED DESCRIPTION +None. + +.SH EXIT STATUS +The +.B sshcd +utility exits with the exit status of the +.BR ssh (1) +utility or with 255 if an error occurred. + +.SH CONSEQUENCES OF ERRORS +Default. + +.SH APPLICATION USAGE +None. + +.SH EXAMPLES +None. + +.SH RATIONALE +For historical reasons, the +.B sshcd +utility does not let the user add a command to run +inside the directory. This also avoids problems; if the +user want to run a command in a specific directory, +.BR sshexec (1) +lets the user do so in an intuitive manner; for other +traditional syntax, +.BR ssh (1) +can still be used — specifying a +.BR cd (1) +command at the beginning shouldn't be a problem. + +.SH NOTES +None. + +.SH BUGS +None. + +.SH FUTURE DIRECTIONS +None. + +.SH SEE ALSO +.BR ssh (1), +.BR sshexec (1) + +.SH AUTHORS +Mattias Andrée +.RI < m@maandree.se > @@ -8,9 +8,12 @@ sshexec - run a command through ssh(1) with normal command syntax .RB [ { .RI [\fBssh=\fP ssh-command ] .RI [\fBdir=\fP directory ] +.RB [ cd= ( strict | lax )] .RB [[\fIfd\fP]{ > , >> , >| , < , <> }[ & ] = \fIfile\fP] +.RB [ asis= \fIasis-marker\fP +.RB [ nasis= \fIasis-count\fP]] .BR } ] -[ssh-option] ...\, +.RI [ ssh-option ]\ ...\, .I destination .I command .RI [ argument ]\ ...\, @@ -20,18 +23,26 @@ The .B sshexec utility is a wrapper for SSH that makes it easy to run commands directly in the SSH command. - +.PP .B sshexec passes any argument after .B } to .I ssh-command .RB ( ssh -if not specified), and only modifies +if not specified), but it +rewrites .I command -.RB [ argument ]\ ...\, -and inserts extra arguments after +and the +.IR argument s +to one argument that can be passed into +.BR ssh (1) +to describe each argument as separate arguments. +It may also rewrite .I destination +to remove information that's not supported by +.BR ssh (1) +and inserts extra arguments after it (it may also add a .B -- argument immediately before @@ -99,6 +110,20 @@ In the remote, change working directory to before executing .IR command . .TP +.B cd=strict +Fail without executing the +.I command +if it's not possible to set +.I directory +as the remote working directory. +.TP +.B cd=lax +Continue (but warn) executing the +.I command +even if it's not possible to set +.I directory +as the remote working directory. +.TP .IB \fR[\fPfd\fP]\fP >= file After changing working directory (assuming one is specified), create or truncate the specified @@ -183,19 +208,52 @@ Close the file descriptor (Default .I fd is 0 (standard input).) +.TP +.BI asis= asis-marker +Any +.I argument +equal to +.I asis-marker +will be skipped over and instead the next argument +(regardless of whether it to is equal to +.IR asis-marker ) +will be interpreted as raw shell code string that +shall be inserted without escaping. +.TP +.BI masis= asis-count +If specified, +.I asis-marker +shall only have it's specified affect up to +.I asis-count +times. .SH OPERANDS The following operands are supported: .TP .I destination -This operand is passed as is (without validation) to the -.BR ssh (1) -utility. The -.BR ssh (1) -utility will expect it the be either in the form -.RI [ user\fP\fB@ ] hostname +The destination to connect and log into. It shall be either in +the form +.RI [ user\fP\fB@ ] hostname [\fB:\fP directory ] or in the form -.BR ssh:// [\fIuser @ ]\fIhostname\fP[ : \fIport\fP]. +.BR ssh [ exec ] :// [\fIuser @ ]\fIhostname\fP[ : \fIport\fP][ / \fIdirectory\fP]. + +.I user +shall be the name of the remote user. If not specified, +the name of the local user running the utility will be used. + +.I hostname +shall be the address to the remote machine. + +.I port +shall be the port or service name for the port to +connect to on the remote machine. + +.I directory +shall be directory to change the remote working directory. +This is an alternative to (with the exact same behaviour) +to the +.B dir +option and cannot be combined with it. .TP .IR command \ [ argument ]\ ...\, Whereas the @@ -211,6 +269,12 @@ utility forces the remote shell to treat each of the as separate arguments and cause the shell to executing them as a non-builtin command. +.I command +must not contain an equals sign +.RB ( = ) +or be just a dash +.RB (\(dq - \(dq). + .SH STDIN The .B sshexec @@ -223,7 +287,7 @@ None. The following environment variables affects the execution of .BR sshexec : .TP -.SH PATH +.I PATH Default. See to the Base Definitions volume of POSIX.1-2017, Section 8.3, Other Environment Variables. This environment variable affects where the .B sshexec @@ -231,6 +295,156 @@ utility can find the .BR ssh (1) utility or .IR ssh-command . +.TP +.I SSHEXEC_SSH +If set and non-empty, it overrides the default value of +.I ssh-command +from +.B ssh +to the value of the variable. +.TP +.I SSHEXEC_OPTS_NO_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that do not have any argument. +(Default is +.BR 46AaCfGgKkMNnqsTtVvXxYy , +meaning the options +.BR -4 , +.BR -6 , +.BR -A , +.BR -a , +.BR -C , +.BR -f , +.BR -G , +.BR -g , +.BR -K , +.BR -k , +.BR -M , +.BR -N , +.BR -n , +.BR -q , +.BR -s , +.BR -T , +.BR -t , +.BR -V , +.BR -v , +.BR -X , +.BR -x , +.BR -Y , +and +.BR -y .) +.TP +.I SSHEXEC_OPTS_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument. +(Default is +.BR BbcDEeFIiJLlmOoPpQRSWw , +meaning the options +.BR -B , +.BR -b , +.BR -c , +.BR -D , +.BR -E , +.BR -e , +.BR -F , +.BR -I , +.BR -i , +.BR -J , +.BR -L , +.BR -l , +.BR -m , +.BR -O , +.BR -o , +.BR -P , +.BR -p , +.BR -Q , +.BR -R , +.BR -S , +.BR -W , +and +.BR -w .) +.TP +.I SSHEXEC_OPTS_OPT_ATTACHED_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument only if +there are additional characters after +the option character in the same +command line argument. (Default is +the empty string, meaning no options.) +.TP +.I SSHEXEC_OPTS_OPT_ARG +List of options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument if there +are additional characters after +the option character in the same +command line argument or if argument is +followed directly by another argument +which does not start with a dash +.RB ( - ). +(Default is +the empty string, meaning no options.) +.TP +.I SSHEXEC_LONG_OPTS_NO_ARG +Space-separated list of long options that +.B sshexec +shall interpret as +.BR ssh (1) +options that do not have any argument +unless it is followed directly by an +equals sign +.RB ( = ) +in the same command line argument. +Options that do not start with two dashes +.RB ( -- ) +are silently ignored. (Default is the +empty string, meaning no options.) +.TP +.I SSHEXEC_LONG_OPTS_ARG +Space-separated list of long options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument that must +either be specified in the next command +line argument or after an +equals sign +.RB ( = ) +the shall directly follow the option +string in the same command line argument. +Options that do not start with two dashes +.RB ( -- ) +are silently ignored. (Default is the +empty string, meaning no options.) +.TP +.I SSHEXEC_LONG_OPTS_OPT_ARG +Space-separated list of long options that +.B sshexec +shall interpret as +.BR ssh (1) +options that have an argument if it is +the option string is is directly followed +by equals sign +.RB ( = ) +in the same command line argument or if +argument is followed directly by another +argument which does not start with a dash +.RB ( - ). +Options that do not start with two dashes +.RB ( -- ) +are silently ignored. (Default is the +empty string, meaning no options.) .PP Other environment variables may affect the execution of the .BR ssh (1) @@ -272,7 +486,9 @@ None. None. .SH RATIONALE -None. +The restrictions on +.I command +is in place to avoid unspecified behaviour. .SH NOTES None. @@ -303,8 +519,9 @@ as special characters. None. .SH SEE ALSO -.BR ssh (1) +.BR ssh (1), +.BR sshcd (1) .SH AUTHORS Mattias Andrée -.RI < maandree@kth.se > +.RI < m@maandree.se > @@ -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 |