8
\$\begingroup\$

One day I used to SSH to lots of machines to do some administrative tasks. I constantly used l to list directories just like I do on my own machines. Guess what, that doesn't work. There were no any usable aliases! So I opened a code editor...

xssh

This script will connect to remote host via ssh and source specified rc file (similar to .bashrc). It is possible to call xssh once again to go deeper, so one may say it is self-reproducing.

I use it for a year or so, code works fine. Things I worry about:

  • fallback behaviour when there is no bash shell
  • base64 is required on host system
  • to be used as alias ssh=xssh script must behave similary to ssh
  • overal code style and consistence
  • in last HEREDOC I try to determine if current shell is bash. If not, I try to fallback to normal shell (w/o ~/.xssh usage). Not sure it is done the right way.

I aim to make this code go public, so I need your review and recommendations.

#!/bin/sh

# xssh - squire for your recursive ssh adventures

# This tiny script will follow you in your ssh adventures,
# just like squires serve their knights.
# It will run commands (think .bashrc) on remote systems and
# follow while you travel from host to host no matter how far
# the road takes you.


# xssh runs ssh and sources your ~/.xssh right after successfull
# login to bash. More over xssh tries to be "self-reproducing"
# so you can use it again to ssh from remote system.
# As stated by shebang xssh doesn't require bash on host
# system but it requires one on the remote.


XSSH_RC=~/.xssh  # Your .xssh file, that will be sourced on remote system
XSSH_SELF="$0"   # Path to this script (change this to absolute path,
                 # if it doesn't work as expected)


SSH_HOST=
SSH_ARGS=

base64_encode() {
    {
        command -v openssl >/dev/null 2>&1 && openssl enc -base64 ||
        command -v base64 >/dev/null 2>&1 && base64
    } | tr -d '\n'
}

base64_decode() {
    sed -e "s/.\{64\}/&\n/g" | {
        command -v openssl >/dev/null 2>&1 && openssl enc -base64 -d ||
        command -v base64 >/dev/null 2>&1 && base64 -d
    }
}

if ! [ -t 1 ]; then
    # Not in terminal, fallback to ssh
    ssh "$@"
    exit $?
fi

if ! [ -r "$XSSH_RC" ] && ! [ -n "$_XSSH_RC" ]; then
    # Nothing to source, fallback to ssh
    ssh "$@"
    exit $?
fi

parse_args() {
    while [ $# -ne 0 ]; do
        case "$1" in
        -b | -c | -D | -E | -e | \
        -F | -I | -i | -L | -l | \
        -m | -O | -o | -p | -Q | \
        -R | -S | -W | -w )
            SSH_ARGS="$SSH_ARGS $1 $2"; shift ;;
        -*)
            SSH_ARGS="$SSH_ARGS $1" ;;
        *)
            if [ -z "$SSH_HOST" ]; then
                SSH_HOST="$1"
            else
                # `command` argument
                # no need to use xssh
                return 1
            fi
        esac
        shift
    done
}

parse_args "$@" || {
    # Failed parsing arguments (`command` argument?), fallback to ssh
    ssh "$@"; exit $?;
}

# Store .xsshrc contents in `$_XSSH_SOURCE`
if [ -z "$_XSSH_RC" ]; then
    export _XSSH_RC=$(cat "$XSSH_RC" | base64_encode)
    { _XSSH_SOURCE=$(base64_encode) ; } <<HEREDOC
_xssh_func() {
    $(cat "$XSSH_SELF")
}
HEREDOC
fi

# Bootstrap script
# This will run .xsshrc and create xssh command
{ _XSSH_INIT=$(base64_encode) ; } <<HEREDOC
base64_decode() {
    sed -e "s/.\{64\}/&\n/g" | {
        command -v openssl >/dev/null 2>&1 && openssl enc -base64 -d ||
        command -v base64 >/dev/null 2>&1 && base64 -d
    }
}
export _XSSH_RC="$_XSSH_RC"
export _XSSH_SOURCE="$_XSSH_SOURCE"
. <(printf '%s' "\$_XSSH_SOURCE" | base64_decode)
. <(printf '%s' "\$_XSSH_RC" | base64_decode)
alias xssh=_xssh_func
HEREDOC

# Snippet to run bootstap with bash shell
# Used with eval, watch for `;`
{ _XSSH_BASH=$(base64_encode) ; } <<HEREDOC
_xssh_init() {
    \$BASH --rcfile <(printf '%s' "$_XSSH_INIT" | base64_decode) -i;
};
HEREDOC

# This script will try to run bash specific code
# If the system is not infected by bash, run login shell instead
{ _XSSH_SAFE_CMD=$(cat) ; } <<HEREDOC
base64_decode() {
    sed -e "s/.\{64\}/&\n/g" | {
        command -v openssl >/dev/null 2>&1 && openssl enc -base64 -d ||
        command -v base64 >/dev/null 2>&1 && base64 -d
    }
};
case "\$SHELL" in
	*/bash*)
		eval \$(printf '%s' "$_XSSH_BASH" | base64_decode)
        ;;
    *)
        test -x "\$SHELL" && exec "\$SHELL" -il || exec /bin/sh -il
        ;;
esac;
_xssh_init;
HEREDOC


# Let the journey begin
ssh -t $SSH_ARGS "$SSH_HOST" "$_XSSH_SAFE_CMD"

Example ~/.xssh rc file:

alias l='ls -hAlt --color=auto --group-directories-first '
alias ssh='xssh '

# I use `vis` editor, it is somewhat simular to `vim`
alias vis=vim

# Up and Down will complete history in bash
bind '"\e[A": history-search-backward'
bind '"\e[B": history-search-forward'

. ~/.bashrc

PS1="\u@\[\033[1;30m\]\h\[\033[0m\] \W "

# I use st terminal, but probably there is no `st.info` on remote host
TERM=xterm-256color

There is simular project sshrc with different goals. In particular xssh tries to be "recursive" and won't leave or create any files (even in /tmp) on remote hosts.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Isn't the logic in if ! [ -r "$XSSH_RC" ] && ! [ -n "_$XSSH_RC" ]; then erroneous? The -n "_$..." has a literal _. \$\endgroup\$
    – hjpotter92
    Apr 21, 2018 at 6:24

1 Answer 1

3
\$\begingroup\$

I think that ssh "$@"; exit $?; can be simply exec ssh "$@". That's objectively better, because now if ssh receives a signal, the parent process (probably your interactive shell) will be informed correctly of which signal and whether a core dump was created. (In any case, exit without an argument will implicitly use $? as status).

Similarly, the final line would be better as exec ssh ….

base64 has a -w or --wrap argument that accepts 0 to disable line-wrapping; openssl -enc -A -base64 also produces single-line output (you'll need to also pass -A on the decode command). That lets you eliminate the tr from encode and the sed from decode.

base64_encode() {
    if openssl exit 2>/dev/null
    then openssl enc -base64 -A
    else base64 -w 0
    fi
}

base64_decode() {
    if openssl exit 2>/dev/null
    then openssl enc -base64 -A -d
    else base64 -d
    fi
}

An answer comment says that we need to work with OpenBSD's base64 which doesn't have the same -w option. That is an excellent reason to include a comment to explain why we're not doing things this obvious way, and prevent future maintainers inadvertently breaking something that works. That's exactly what comments are good for!

You can save re-typing these definitions in the here-documents, by extracting their definitions from set output (but only if you make this a Bash script):

$(set | sed -n '/^base64_..code /,/^}/p')

Did you consider printf %q as an alternative encoding? I think that's specific to GNU coreutils, so might not be acceptable to you. In particular, you'd need to avoid the built-in printf in shells such as Dash.

The example file really shouldn't have terminal-specific codes before you've tested $TERM. Preferably, use tput to insert the appropriate codes for whatever terminal is found.

\$\endgroup\$
2
  • \$\begingroup\$ IIRC OpenBSD base64 -w doesn't work similar. Thats the reason tr and sed landed there. Nice trick about HEREDOC, but I am bound to sh shell (on host system). printf %q may be a nice player, but I am not sure how many systems have both bash and GNU coreutils together. Doesn't quite understand your last paragraph. \$\endgroup\$
    – sineemore
    Apr 23, 2018 at 11:18
  • \$\begingroup\$ The compatibility considerations are definitely worth commenting in your code (to prevent future readers making the same suggestions as me). Remember the mantra: "explain why; make how explain itself." To explain the last paragraph - \033[1;30m and \033[0m are VT-style terminal escape codes. Not all terminals are VT, so use tput to generate the correct codes instead of those hard-coded strings. I've just noticed override of TERM to something that may be wrong just afterwards - that looks risky, too. \$\endgroup\$ Apr 23, 2018 at 11:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.