Created
November 19, 2012 01:40
-
-
Save tarao/4108520 to your computer and use it in GitHub Desktop.
auth-command - a tool for SSH authorized commands
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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