Sometimes you want to run a bit more than just one command over ssh, and you also want to maintain interactivity to the processes that you launch. Usually you could just do
ssh -t <host> "foo; bar; fish; paste"
however that can quickly become unwieldly, especialy if it is more than just a sequence of commands. You also can't use
cat my_script | ssh <host> /bin/bash
because then you loose your stdin
and thus your interactivity.
However, there is a way to achieve our goal using the first form above. Consider that the maximum command line length is usually more than 2MB (see getconf ARG_MAX
). Thus, we convert our entire script into a base64 encoded string with cat my_script | base64
and then call
ssh -t <host> /bin/bash "<(echo <base64 string> | base64 --decode)"
.
In this way the entire script is sent to the host in the command buffer, and so long as our total command length is less than 2MB, it will run.
Note: the usage of the <(...)
(Process Substitution), and the quotes to prevent it from running locally.
So, now expressing that in full form would be:
ssh -t <host> /bin/bash "<(echo "$(cat my_script | base64)" | base64 --decode)" <arg1> ...
Though to avoid using the echo, which feels a bit klunky, lets rather use a <<EOF
(Here Document) with cat
and also use $'...'
(ANSI C Quoting) for precise control, which gives us:
ssh -t <host> /bin/bash $'<(cat<<_ | base64 --decode\n'$(cat my_script | base64 | tr -d "\n")$'\n_\n)' <arg1> ...
Edit: a shorter form not using
cat
and making use of a Here String gives a:ssh -t <host> /bin/bash '<(base64 --decode <<<'$(cat my_script | base64 |tr -d "\n")')'
Note: the tr -d "\n"
is helpful as some base64 decode implementations will fail on the whitespace. I used a _
as the EOF marker as it is not a character in the base64 set.
This idea is developing quite nicely, however it still falls short in some ways. What if say we want to run a Python script? We'd have to change to ssh -t <host> /usr/bin/env python ...
. And if we wanted to provide arguments to our script that are actually input files, how would we get those across the wire?
Considering the above and more, after some further tinkering we can arrive at sshx (see full source below) which will marshall up an entire command line and run it over ssh as follows.
Say we have a Python script called "foo":
#!/usr/bin/env python
import sys
arg1 = sys.argv[1]
print "The contents of arg1:"
sys.stdout.write(open(arg1).read())
And a file called "hi":
Hello World!
To run $ foo hi
on a remote host we could generate the following BASH fragment:
export SCRATCH=$(TMPDIR=$HOME mktemp -d -t .scratch.XXXXXXXX)
trap "{ rm -rf \"$SCRATCH\"; }" EXIT SIGINT SIGTERM SIGKILL
chmod 700 $SCRATCH
# cmd: foo hi
# file: "foo" -> "$SCRATCH/foo"
cat<<_ |base64 --decode >"$SCRATCH/foo"; chmod 755 "$SCRATCH/foo"; touch -d @1557867114 "$SCRATCH/foo"
IyEvdXNyL2Jpbi9lbnYgcHl0aG9uCmltcG9ydCBzeXMKYXJnMSA9IHN5cy5hcmd2WzFdCnByaW50ICJUaGUgY29udGVudHMgb2YgYXJnMToiIApzeXMuc3Rkb3V0LndyaXRlKG9wZW4oYXJnMSkucmVhZCgpKQo=
_
# file: "hi" -> "$SCRATCH/hi"
cat<<_ |base64 --decode >"$SCRATCH/hi"; chmod 644 "$SCRATCH/hi"; touch -d @1557867136 "$SCRATCH/hi"
SGVsbG8gV29ybGQhCg==
_
$SCRATCH/foo $SCRATCH/hi
Then we further base64 encode this marshalled command and use the above technique to run the command over ssh.
sshx:
#!/bin/bash
_realpath() {
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}
_stat_mode() {
if [[ "$OSTYPE" == darwin* ]]; then
stat -f %Lp "$1"
else
stat --format %a "$1"
fi
}
_stat_mtime() {
if [[ "$OSTYPE" == darwin* ]]; then
stat -f '%m' "$1"
else
stat -c '%Y' "$1"
fi
}
cat_base64_file() {
local FILE=${1:?[?FILE]}
echo "cat<<_ |base64 --decode" "${@:2}"
cat "$FILE" | base64
echo "_"
}
is_marshalable_file() {
local FILE=${1:?[?FILE]}
if [ -e "$FILE" -a ! -d "$FILE" ] && [[ "$FILE" =~ ^/dev/fd/* || "$(_realpath "$FILE")" =~ ^/tmp/*|^/home/*|^/Users/* ]]; then
return 0
else
return 1
fi
}
match_count() {
local HAYSTACK=${1:?[?HAYSTACK]}
local NEEDLE=${2:?[?NEEDLE]}
local X=${HAYSTACK//$NEEDLE}
echo $(((${#HAYSTACK} - ${#X}) / ${#NEEDLE}))
}
cat_marshalled_command() {
cat <<"EOF"
export SCRATCH=$(TMPDIR=$HOME mktemp -d -t .sshx-scratch.XXXXXXXX)
trap "{ rm -rf \"$SCRATCH\"; }" EXIT SIGINT SIGTERM SIGKILL
chmod 700 $SCRATCH
EOF
echo "# cmd: ${@}"
local ARGS=""
local FILES=" "
while [ -n "$1" ]; do
if is_marshalable_file "$1"; then
if [[ "$1" == /dev/fd/* ]]; then
FILENAME="~dev~fd~$(basename "$1")"
MODE="500"
elif [ -p "$1" ]; then
FILENAME="$(basename "$1")"
MODE="500"
else
FILENAME="$(basename "$1")"
MODE=$(_stat_mode "$1")
fi
FILE='$SCRATCH/'$FILENAME
MTIME=$(_stat_mtime "$1")
# Count the repeated file names and suffix index as necessary
COUNT=$(match_count "$FILES" "$FILE")
FILES=$FILES"$FILE "
if [ "$COUNT" != "0" ]; then
FILE=$FILE"~$COUNT"
fi
echo "# file: \"$1\" -> \"$FILE\""
cat_base64_file "$1" ">\"$FILE\"; chmod ${MODE} \"$FILE\"; touch -d @$MTIME \"$FILE\""
ARGS=$ARGS"$FILE "
else
ARGS=$ARGS$(printf '%q ' "$1")
fi
shift
done
cat <<EOF
$ARGS
EOF
}
sshx() {
HOST=${1:?[?HOST]}
if [ "$HOST" == "stdout" ]; then
cat_marshalled_command "${@:2}"
exit 0
fi
unset TTY; [ -t 1 ] && TTY="-t"
ssh $TTY $HOST \
/bin/bash '<(base64 --decode <<<'$(cat_marshalled_command "${@:2}"|base64|tr -d "\n")')'
}
sshx "${@}"
your initial examples are interesting. but could be improved.
you are making use of "cat" which is not a bash builtin, and completely unnecessary. (look up "useless use of cat"!
Why use '<<' here-files and the 'tr' and other junk.... Why not bypass that all together using '<<<' here-strings
Example...
ssh -t host "/bin/bash <(base64 --decode <<<'$( base64 < myscript )' )"
This will do the same thing. is smaller, more elegant. and must easier to read and understand (decoding right to left)