diff --git a/README.md b/README.md index 54ca264..714905d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # sshukh zsh plugin + *SSH Update Known Hosts* Prompts to update `known_hosts` file for you if needed. @@ -6,25 +7,33 @@ Prompts to update `known_hosts` file for you if needed. ## Installation: **With Oh-my-zsh:** run the following command: + ``` $ git clone https://github.com/anatolykopyl/sshukh.git $HOME/.oh-my-zsh/custom/plugins/sshukh ``` + **With Antigen:** add the following to your `.zshrc`: + ``` antigen bundle anatolykopyl/sshukh ``` To always use with ssh (recommended) add this alias to your `.zshrc`: + ``` alias ssh='sshukh' ``` + Zsh may tell you that the plugin is disabled until permissions are fixed. Just run the command it suggests. + ``` $ compaudit | xargs chmod g-w,o-w ``` ## Usage + Let's connect to an ip that changed hosts: + ``` $ sshukh pi@192.168.1.54 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@ -45,4 +54,5 @@ Update known_hosts? [y/n] y /Users/akopyl/.ssh/known_hosts updated. Original contents retained as /Users/akopyl/.ssh/known_hosts.old ``` -There is a prompt asking if you want to remove the conflicting host from `known_hosts` and upon answering `y` ssh reruns automatically successfully. + +There is a prompt asking if you want to remove the conflicting host from `known_hosts` and upon answering `y` ssh reruns automatically successfully. \ No newline at end of file diff --git a/sshukh.plugin.zsh b/sshukh.plugin.zsh index 89161e0..f3868e0 100755 --- a/sshukh.plugin.zsh +++ b/sshukh.plugin.zsh @@ -2,7 +2,7 @@ # Description # ----------- # -# User will be prompted if they want to update known_hosts if ssh errors out +# User will be prompted if they want to update known_hosts if ssh errors out # with "Host key verification failed." # # ------------------------------------------------------------------------------ @@ -13,18 +13,130 @@ # # ------------------------------------------------------------------------------ -sshukh () { - output=$(\ssh "$@" 2>&1 | tee /dev/tty) - error=$(echo $output | tail -1) - if [[ "$error" == "Host key verification failed."* ]]; then - host=$(cut -d'@' -f2 <<< $1) - while true; do - read yn"?Update known_hosts? [y/n] " - case $yn in - [Yy]* ) ssh-keygen -R $host && \ssh "$@"; break;; - [Nn]* ) break;; - * ) echo "Please answer y or n.";; - esac - done - fi +# OpenSSH prints " host key for has changed." — use that host +# for ssh-keygen -R (correct for SSH config Host aliases that resolve to IPs). +_sshukh_offending_host_from_output() { + print -r -- "$1" | command grep -E -m1 ' host key for .+ has changed' | command sed -E 's/.* host key for ([^ ]+) has changed.*/\1/' +} + +# Walk ssh argv like ssh(1): options first, then destination [command ...]. +# Sets _sshukh_prefix (options only) and _sshukh_dest (user@host or alias). +_sshukh_split_ssh_args() { + local i a + local -a args + args=("$@") + _sshukh_prefix=() + _sshukh_dest="" + + i=1 + while (( i <= $#args )); do + a="${args[i]}" + case $a in + --) + _sshukh_prefix+=("$a") + (( i++ )) + (( i <= $#args )) || return 1 + _sshukh_dest="${args[i]}" + return 0 + ;; + -[46AaCfGgKkMNnqsTtVvXxYy]) + _sshukh_prefix+=("$a") + ;; + -v*) + _sshukh_prefix+=("$a") + ;; + -B|-b|-c|-E|-e|-F|-I|-i|-J|-L|-l|-m|-O|-P|-p|-Q|-R|-S|-W|-w) + _sshukh_prefix+=("$a") + (( i++ )) + (( i <= $#args )) || return 1 + _sshukh_prefix+=("${args[i]}") + ;; + -D) + _sshukh_prefix+=("$a") + (( i++ )) + (( i <= $#args )) || return 1 + _sshukh_prefix+=("${args[i]}") + ;; + -D*|-B*|-b*|-c*|-E*|-e*|-F*|-I*|-i*|-J*|-L*|-l*|-m*|-O*|-P*|-p*|-Q*|-R*|-S*|-W*|-w*) + _sshukh_prefix+=("$a") + ;; + -o) + _sshukh_prefix+=("$a") + (( i++ )) + (( i <= $#args )) || return 1 + _sshukh_prefix+=("${args[i]}") + ;; + -o*) + _sshukh_prefix+=("$a") + ;; + -*) + _sshukh_prefix+=("$a") + ;; + *) + _sshukh_dest="$a" + return 0 + ;; + esac + (( i++ )) + done + return 1 +} + +# Hostnames to try with ssh-keygen -R: offending line from ssh, then config +# alias / canonical names from ssh -G (covers known_hosts under IP vs name). +_sshukh_hosts_for_keygen() { + local output h stripped dest canon hostpat gout + output="$1" + shift + + typeset -U hosts + hosts=() + + h=$(_sshukh_offending_host_from_output "$output") + [[ -n "$h" ]] && hosts+=("$h") + + if _sshukh_split_ssh_args "$@"; then + dest="$_sshukh_dest" + stripped="${dest#*@}" + [[ "$stripped" == "$dest" ]] && stripped="$dest" + [[ -n "$stripped" ]] && hosts+=("$stripped") + + gout=$(\ssh -G "${_sshukh_prefix[@]}" "$dest" 2>/dev/null) || gout="" + if [[ -n "$gout" ]]; then + canon=$(print -r -- "$gout" | command awk '/^hostname / { print $2; exit }') + hostpat=$(print -r -- "$gout" | command awk '/^host / { print $2; exit }') + [[ -n "$canon" ]] && hosts+=("$canon") + [[ -n "$hostpat" ]] && hosts+=("$hostpat") + fi + fi + + print -rl -- "${hosts[@]}" +} + +sshukh () { + local output error host + + output=$(\ssh "$@" 2>&1 | tee /dev/tty) + error=$(print -r -- "$output" | tail -n1) + + if [[ "$error" != *"Host key verification failed"* ]]; then + return 0 + fi + + while true; do + read yn"?Update known_hosts? [y/n] " + case $yn in + [Yy]* ) + while IFS= read -r host; do + [[ -z "$host" ]] && continue + \ssh-keygen -R "$host" 2>/dev/null + done < <(_sshukh_hosts_for_keygen "$output" "$@") + + \ssh "$@" + break + ;; + [Nn]* ) break;; + * ) echo "Please answer y or n.";; + esac + done }