Skip to content

Instantly share code, notes, and snippets.

@tarao
Created November 19, 2012 01:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tarao/4108520 to your computer and use it in GitHub Desktop.
Save tarao/4108520 to your computer and use it in GitHub Desktop.
auth-command - a tool for SSH authorized commands
#!/bin/sh -e
base=`basename "$0"`
auth_options=',no-port-forwarding,no-agent-forwarding'
lf='
'
duser=`id -run`
dport=22
dconfig_dir_rel=".ssh/$base/config"
dconfig="$HOME/$dconfig_dir_rel"
test=0
verbose=0
help() {
cat <<EOF
Usage: $base init OPTIONS
$base add OPTIONS <host> <name> <command> <arg>...
$base add OPTIONS <host> <name> -s <script> <arg>...
$base del OPTIONS <name>
$base run OPTIONS <name> <arg>...
Options:
-l <user> Login user name. (default: $duser)
-p <port> Port number. (default: $dport)
-X Enable X11 forwarding. (default: false)
-c <config> SSH configuration file. (default: ~/$dconfig_dir_rel)
-d <dest> Path to save script in the remote host.
-t Request tty.
-v Verbose messages.
--test Test mode; print what will be done instead of actually doing it.
Options for add:
-i <name>[:<file>]
Inherit config entry of host <name> in a config file <file>.
-I <name>[:<file>]
Include config entry of host <name> in a config file <file>.
Options for del:
-s Remove remote script file.
-k Remove key files.
-a Remove remote script file and key files.
Arguments:
<host> The remote host name.
<name> Name of the command and key file.
<command> Command to run.
-s <script> Script to run.
EOF
exit $1
}
testing() {
[ $test != 0 ] && return 0
}
sed_escape() {
echo "$1" | sed -e 's/\//\\\//g' | sed -e 's/\([*]\)/\\\1/g'
}
shell_escape() {
printf %s\\n "$1" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"
}
shell_escape2() {
pat1="s/\([^-A-Za-z0-9_.,:\/@\n]\)/\\\\\\\\\1/g"
pat2="s/\(\"\)/\\\\\1/g"
pat3="s/\('\)/'\"'\"'/g"
echo "$1" | sed -e "$pat1" | sed -e "$pat2" | sed -e "$pat3"
}
run() {
cmd=$(shell_escape "$1"); shift
for x in "$@"; do
[ "x$x" = 'x|' ] && {
cmd="$cmd |"
} || {
cmd="$cmd $(shell_escape "$x")"
}
done
testing && echo "$cmd" || eval "$cmd"
}
ssh_request_tty_available() {
msg='missing argument'
command ssh -o RequestTTY 2>&1 | grep "$msg" > /dev/null && return 0
}
ssh() {
args=''
for x in "$@"; do
args="$args $(shell_escape "$x")"
done
eval "command ssh $ssh_options$args"
}
run_ssh() {
args=''
for x in "$@"; do
args="$args $(shell_escape "$x")"
done
sshcmd="exec ssh $ssh_options$args"
testing && echo "$sshcmd" || eval "$sshcmd"
}
info() {
[ $verbose != 0 ] && echo $1 >&2
return 0
}
error() {
echo "$1" >&2
exit 1
}
argument_error() {
echo "$1" >&2
help 1
}
argument_require_action() {
opt="$1"; shift
valid_action=0
for x in $@; do
[ "x$x" = "x$action" ] && valid_action=1
done
[ $valid_action = 1 ] || argument_error "$opt: requires action \"$1\""
return 0
}
argument_require() {
[ -z "$1" ] && argument_error "$2: insufficient arguments"
return 0
}
read_entry() {
filter="/^\\s*$2\\s\\+/p"
extract="s/^\\s*$2\\s\\+\\(.*\\)$/\\1/"
echo "$1" | sed -n -e "$filter" | sed -e "$extract"
}
filter_entry() {
filter="/^\\s*$2\\s\\+/d"
echo "$1" | sed -e "$filter"
}
load_config() {
entry_name=$(sed_escape "$1")
file="$2"
[ -z "$file" ] && file="$config"
[ -f "$file" ] || {
info "File not found: \"$file\""
return 0
}
sed -n -e ":begin
/^Host $entry_name\$/{
:loop
p
n
/^Host /!b loop
b begin
}
d" "$file"
}
read_config1() {
entry=$(load_config "$1")
[ -z "$user" ] && user=$(read_entry "$entry" 'User')
[ -z "$host" ] && host=$(read_entry "$entry" 'HostName')
[ -z "$port" ] && port=$(read_entry "$entry" 'Port')
[ -z "$x11" ] && x11=$(read_entry "$entry" 'ForwardX11')
[ -z "$tty" ] && tty=$(read_entry "$entry" 'RequestTTY')
[ -z "$script" ] && script=$(read_entry "$entry" '#Script')
[ -z "$identity" ] && identity=$(read_entry "$entry" 'IdentityFile')
[ "x$x11" = 'xno' ] && x11=''
[ "x$tty" = 'xno' ] && tty=''
return 0
}
read_config() {
read_config1 "$name"
read_config1 '*'
}
make_config() {
info "Make configuration directory"
run mkdir -p "$dir"
run chmod go-rwx "$dir"
cuser=''; cport=''; cx11=''; ctty=" RequestTTY no$lf"
[ "$action" = 'init' ] && {
[ -n "$user" ] && cuser=" User $user$lf"
[ -n "$port" ] && cport=" Port $port$lf"
[ -n "$x11" ] && cx11=" ForwardX11 yes$lf"
[ -n "$tty" ] && ctty=" RequestTTY yes$lf"
}
ssh_request_tty_available || ctty=''
info "Initialize \"$config\""
data=$(cat <<EOF
Host *
${cuser}${cport}${cx11}${ctty} PreferredAuthentications publickey
IdentitiesOnly yes
EOF
)
for x in $include; do
[ -z "$x" ] && continue
entry_name=$(echo "$x:" | cut -f 1 -d :)
file_name=$(echo "$x:" | cut -f 2 -d :)
[ -z "$file_name" ] && file_name="$HOME/.ssh/config"
info "Load \"$entry_name\" in \"$file_name\""
entry=$(load_config "$entry_name" "$file_name")
data="$data$lf$entry"
done
testing && echo "cat > $config <<EOF
$data
EOF"
testing || cat > $config <<EOF
$data
EOF
}
make_key() {
info "Generate SSH key \"$dir/$name[.pub]\""
keydir=`dirname "$dir/$name"`
run mkdir -p "$keydir"
testing && echo ssh-keygen -t rsa -N '' -C "$comment" -f "$dir/$name"
testing || ssh-keygen -t rsa -N '' -C "$comment" -f "$dir/$name"
}
remove_key() {
[ -z "$identity" ] && identity="$dir/$name"
[ -f "$identity" ] || error 'No identity file found'
info "Remove SSH key \"$dir/$name[.pub]\""
run rm "$identity"
run rm "$identity.pub"
}
unregister_key() {
entry_name=$(sed_escape "$name")
sed_script=":begin
/^Host $entry_name\$/{
:loop
n
/^Host /!b loop
b begin
}
p
d"
testing && echo "sed -n -e \"$sed_script\" \"$config\" > \"$config.tmp\""
testing || sed -n -e "$sed_script" "$config" > "$config.tmp"
run mv "$config.tmp" "$config"
}
register_key() {
unregister_key
info "Register \"$name\" to \"$config\""
entry=''
[ -n "$inherit" ] && {
entry_name=$(echo "$inherit:" | cut -f 1 -d :)
file_name=$(echo "$inherit:" | cut -f 2 -d :)
[ -z "$file_name" ] && file_name="$HOME/.ssh/config"
info "Inherit \"$entry_name\" in \"$file_name\""
entry=$(load_config "$entry_name" "$file_name" | sed -e '1d')
entry=$(filter_entry "$entry" 'IdentityFile')
entry="$entry$lf"
}
entry_host=$(read_entry "$entry" 'HostName')
chost=''; cuser=''; cport=''; cx11=''; ctty=''
[ -z "$entry_host" ] && chost=" HostName $host$lf"
[ -n "$user" ] && {
cuser=" User $user$lf"
entry=$(filter_entry "$entry" 'User')
}
[ -n "$port" ] && {
cport=" Port $port$lf"
entry=$(filter_entry "$entry" 'Port')
}
[ -n "$x11" ] && {
cx11=" ForwardX11 yes$lf"
entry=$(filter_entry "$entry" 'ForwardX11')
}
[ -n "$tty" ] && ssh_request_tty_available && {
ctty=" RequestTTY yes$lf"
entry=$(filter_entry "$entry" 'RequestTTY')
}
[ -n "$script" ] && {
cscript=" #Script $path$lf"
entry=$(filter_entry "$entry" '#Script')
}
data=$(cat <<EOF
Host $name
${entry}${chost}${cuser}${cport}${cx11}${ctty} IdentityFile $dir/$name
${cscript}
EOF
)
testing && echo "cat >> $config <<EOF
$data
EOF"
testing || cat >> $config <<EOF
$data
EOF
}
authorize_code() {
auth_dir="~/.ssh"
auth="$auth_dir/authorized_keys"
auth_new="$auth_dir/authorized_keys.new"
auth_old="$auth_dir/authorized_keys.bak"
[ -z "$tty" ] && auth_options=",no-pty$auth_options"
cat <<EOF
[ $verbose != 0 ] && echo "Authorize command=\\"\$cmd$args\\" for \\"$comment\\"" >&2
cmd="command=\\"\$cmd$args\\"$auth_options"
[ -d $auth_dir ] || {
mkdir -p $auth_dir
chmod go-rwx $auth_dir
}
touch $auth
sed -e "/ $(sed_escape "$comment")\$/d" $auth > $auth_new
echo "\$cmd $(cat "$dir/$name.pub")" >> $auth_new
mv $auth $auth_old
mv $auth_new $auth
chmod go-rwx $auth
EOF
}
send_script() {
rdir=$(dirname "$dconfig_dir_rel")/script
rpath="$rdir/$path"
info "Installing \"$script\" to \"$host:$rpath\" ..."
authorize=$(authorize_code)
run=$(cat <<EOF
mkdir -p "$rdir"
rm -rf "$rpath"
touch "$rpath"
chmod a+x "$rpath"
while IFS= read -r line; do
echo "\$line" >> "$rpath"
done
cmd=\$(readlink -f "$rpath")
$authorize
EOF
)
shost="$host"
[ -n "$port" ] && sport="-p $port"
[ -n "$user" ] && shost="$user@$host"
cmd="/bin/sh -c '$run'"
run cat "$script" \| ssh $sport "$shost" "$cmd" && {
info 'done'
} || info 'abort'
}
send_command() {
info "Setting remote command \"$command\" in $host ..."
authorize=$(authorize_code)
run=$(cat <<EOF
if type "$command" >/dev/null 2>&1; then
cmd=\$(which "$command")
elif [ -x "$command" ]; then
cmd=\$(readlink -f "$command")
else
echo "Command \\"$command\\" not found" >&2
exit 1
fi
$authorize
EOF
)
shost="$host"
[ -n "$port" ] && sport="-p $port"
[ -n "$user" ] && shost="$user@$host"
cmd="/bin/sh -c '$run'"
run ssh $sport "$shost" "$cmd" && {
info 'done'
} || info 'abort'
}
remove_remote_entry() {
[ -z "$host" ] && return 0
auth="~/.ssh/authorized_keys"
auth_new="~/.ssh/authorized_keys.new"
auth_old="~/.ssh/authorized_keys.bak"
info "Unsetting remote command in $host ..."
[ -n "$del_script" ] && [ -n "$script" ] && {
remove_script=$(cat<<EOF
[ $verbose != 0 ] && echo "Remove \"$script\"" >&2
[ -f "$script" ] && rm "$script"
EOF
)
}
run=$(cat <<EOF
$remove_script
[ $verbose != 0 ] && echo "Unauthorize \\"$comment\\"" >&2
sed -e "/ $(sed_escape "$comment")\$/d" $auth > $auth_new
mv $auth $auth_old
mv $auth_new $auth
chmod go-rwx $auth
EOF
)
shost="$host"
[ -n "$port" ] && sport="-p $port"
[ -n "$user" ] && shost="$user@$host"
cmd="/bin/sh -c '$run'"
run ssh $sport "$shost" "$cmd" && {
info 'done'
} || info 'abort'
}
action="$1"
[ -z "$action" ] && help
shift
parsing=1
while [ $parsing = 1 ] && [ -n "$1" ]; do
case "$1" in
-l) shift
user="$1"; argument_require "$1" '-u <user>'; shift
;;
-p) shift
port="$1"; argument_require "$1" '-p <port>'; shift
;;
-X) shift
x11='yes'
;;
-c) shift
config="$1"; argument_require "$1" '-c <config>'; shift
;;
-d) shift
path="$1"; argument_require "$1" '-d <dest>'; shift
;;
-t) shift
tty='yes'
;;
-o) shift
ssh_options="$ssh_options -o $(shell_escape "$1")"
argument_require "$1" '-o <option>'; shift
;;
-v) shift
verbose=1
;;
--test)
shift
test=1
;;
-i) shift
argument_require_action '-i' 'add'
inherit="$1"; argument_require "$1" '-i <name>'; shift
;;
-I) shift
argument_require_action '-I' 'add' 'init'
include="$include$lf$1"; argument_require "$1" '-I <name>'; shift
;;
-s) shift
argument_require_action '-s' 'del'
del_script=1
;;
-k) shift
argument_require_action '-k' 'del'
del_key=1
;;
-a) shift
argument_require_action '-a' 'del'
del_script=1
del_key=1
;;
*) parsing=0
;;
esac
done
[ -z "$config" ] && config="$dconfig"
dir=`dirname "$config"`
case "$action" in
add|init)
[ "$action" = 'add' ] && {
host="$1"; argument_require "$1" "add <host>"; shift
name="$1"; argument_require "$1" "add $host <name>"; shift
[ "x$1" = 'x-s' ] && {
shift
script="$1"; argument_require "$1" "add $host $name -s <script>"
[ -z "$path" ] && path=`basename "$script"`
}
command="$1"; argument_require "$1" "add $host $name <command>"; shift
[ -n "$path" ] && [ -z "$script" ] && {
argument_error "$base -d \"$path\": must be used with -s <script>"
}
args=''
for arg in "$@"; do
args="$args $(shell_escape2 "$arg")"
done
localhost=`hostname`
comment="auth-command:$name@$localhost"
}
# make SSH config
[ -f "$config" ] && [ "$action" != 'init' ] || make_config
[ "$action" = 'init' ] && exit 0
# make SSH key and config entry
[ -f "$dir/$name" ] || make_key
grep "^Host\s\+$name\$" "$config" >/dev/null 2>/dev/null || register_key
read_config
if [ -n "$script" ]; then
send_script
else # command
send_command
fi
;;
del)
name="$1"; argument_require "$1" 'del <name>'; shift
localhost=`hostname`
comment="auth-command:$name@$localhost"
[ -n "$1" ] && argument_error "$base del: unknown argument \"$1\""
[ -f "$config" ] && read_config
remove_remote_entry "$1"
info "Unregister \"$name\" from \"$config\""
unregister_key
[ -n "$del_key" ] && remove_key
;;
run)
name="$1"; argument_require "$1" 'run <name>'; shift
[ -f "$config" ] || error "$base run: cannot read \"$config\""
read_config
[ -n "$user" ] && user="-l $user"
[ -n "$port" ] && port="-p $port"
[ -n "$x11" ] && x11='-X'
[ -n "$tty" ] && tty='-t' || tty='-T'
SSH_AUTH_SOCK= run_ssh -F "$config" $x11 $tty $port $user "$name" "$@"
# "$@" goes to SSH_ORIGINAL_COMMAND
;;
help)
help
;;
*) argument_error "Unknown action: \"$action\""
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment