fix: confirm SSH alias updates and keep key handling consistent
This commit is contained in:
+208
-58
@@ -22,9 +22,9 @@ function __ff_ssh_ensure_keys
|
||||
|
||||
echo "No SSH key found, generating ed25519: $key_path"
|
||||
if test -t 0
|
||||
ssh-keygen -t ed25519 -f $key_path -C $comment
|
||||
ssh-keygen -t ed25519 -f $key_path -C $comment; or return 1
|
||||
else
|
||||
ssh-keygen -t ed25519 -f $key_path -C $comment -N ""
|
||||
ssh-keygen -t ed25519 -f $key_path -C $comment -N ""; or return 1
|
||||
echo "Non-interactive mode: created with empty passphrase"
|
||||
end
|
||||
end
|
||||
@@ -313,7 +313,7 @@ function __ff_ssh_gval --argument-names key
|
||||
end
|
||||
end
|
||||
|
||||
# File that already declares `Host <alias>` (main config or config.d)
|
||||
# Files declaring an exact alias; repeated declarations need manual disambiguation.
|
||||
function __ff_ssh_find_alias_file --argument-names alias_name
|
||||
set -l files $HOME/.ssh/config
|
||||
if test -d $HOME/.ssh/config.d
|
||||
@@ -321,21 +321,63 @@ function __ff_ssh_find_alias_file --argument-names alias_name
|
||||
end
|
||||
for f in $files
|
||||
test -f $f; or continue
|
||||
if awk -v want=(string lower -- $alias_name) '
|
||||
tolower($1) == "host" {
|
||||
for (i = 2; i <= NF; i++)
|
||||
if (tolower($i) == want) hit = 1
|
||||
awk -v want=(string lower -- $alias_name) '
|
||||
tolower($1) == "host" {
|
||||
for (i = 2; i <= NF && $i !~ /^#/; i++) {
|
||||
if (tolower($i) == want) { print FILENAME; break }
|
||||
}
|
||||
END { exit(hit ? 0 : 1) }
|
||||
' $f
|
||||
echo $f
|
||||
return 0
|
||||
end
|
||||
}
|
||||
' $f
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
# 复制公钥到远端(ssh-copy-id),失败不阻断配置写入
|
||||
# Replace managed values in place; split shared Host declarations for other aliases.
|
||||
function __ff_ssh_replace_alias --argument-names alias_name original replacement
|
||||
awk -v want=(string lower -- $alias_name) -v replacement="$replacement" '
|
||||
BEGIN {
|
||||
while ((getline line < replacement) > 0) block = block line "\n"
|
||||
close(replacement)
|
||||
}
|
||||
tolower($1) == "host" || tolower($1) == "match" {
|
||||
updating = 0
|
||||
if (tolower($1) == "host") {
|
||||
hit = 0; others = ""; comment = ""
|
||||
for (i = 2; i <= NF; i++) {
|
||||
if ($i ~ /^#/) {
|
||||
for (; i <= NF; i++) comment = comment " " $i
|
||||
break
|
||||
}
|
||||
if (tolower($i) == want) hit = 1
|
||||
else others = others " " $i
|
||||
}
|
||||
if (hit) {
|
||||
printf "%s", block
|
||||
if (others != "") print "Host" others " !" want comment
|
||||
else {
|
||||
updating = 1
|
||||
if (comment != "") print " " comment
|
||||
}
|
||||
next
|
||||
}
|
||||
}
|
||||
}
|
||||
updating && tolower($1) ~ /^(hostname|user|port|identityfile|proxyjump|proxycommand)$/ { next }
|
||||
{ print }
|
||||
' "$original"
|
||||
end
|
||||
|
||||
# Normalize ssh -G paths (including explicit .pub) to a private-key path.
|
||||
function __ff_ssh_private_path --argument-names identity
|
||||
set identity (string replace -r '^~/' "$HOME/" -- $identity)
|
||||
set identity (string replace -a '%d' "$HOME" -- $identity)
|
||||
set identity (string replace -r '\.pub$' '' -- $identity)
|
||||
if not string match -q '/*' -- $identity
|
||||
set identity "$PWD/$identity"
|
||||
end
|
||||
printf '%s\n' "$identity"
|
||||
end
|
||||
|
||||
# Copy only the selected key; upstream can return 0 after an interrupted filter.
|
||||
function __ff_ssh_copy_id
|
||||
set -l target $argv[1]
|
||||
set -l port $argv[2]
|
||||
@@ -344,27 +386,36 @@ function __ff_ssh_copy_id
|
||||
set -l proxycommand $argv[5]
|
||||
|
||||
if not type -q ssh-copy-id
|
||||
echo "ff ssh: 未找到 ssh-copy-id,跳过公钥复制(可手动执行 ssh-copy-id $target)" >&2
|
||||
echo "ff ssh: 未找到 ssh-copy-id;未写入 Host(可用 --no-copy 只写配置)" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set -l cmd ssh-copy-id
|
||||
if test -n "$port"; and test "$port" != 22
|
||||
set cmd $cmd -p $port
|
||||
end
|
||||
if test -n "$identity"
|
||||
set cmd $cmd -i $identity
|
||||
end
|
||||
set -l options -o ConnectTimeout=10 -o ConnectionAttempts=1
|
||||
if test -n "$proxyjump"
|
||||
set cmd $cmd -o "ProxyJump=$proxyjump"
|
||||
set options $options -o "ProxyJump=$proxyjump"
|
||||
end
|
||||
if test -n "$proxycommand"
|
||||
set cmd $cmd -o "ProxyCommand=$proxycommand"
|
||||
set options $options -o "ProxyCommand=$proxycommand"
|
||||
end
|
||||
set cmd $cmd $target
|
||||
|
||||
set -l cmd ssh-copy-id -i "$identity.pub" -p "$port" $options "$target"
|
||||
echo "复制公钥: $cmd"
|
||||
$cmd
|
||||
# The wrapper records SIGINT even if ssh-copy-id's cleanup trap exits 0.
|
||||
command sh -c 'trap "exit 130" INT; trap "exit 143" TERM; "$@"' ff-ssh $cmd
|
||||
if test $status -ne 0
|
||||
echo "ff ssh: 公钥复制失败或已取消,未写入 Host" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
# A skipped-key warning alone proves nothing. Exclude inherited identities,
|
||||
# certificates and multiplexed sessions; the agent may unlock this key only.
|
||||
command ssh -F /dev/null -i "$identity" -p "$port" $options \
|
||||
-o BatchMode=yes -o PreferredAuthentications=publickey \
|
||||
-o IdentitiesOnly=yes -o CertificateFile=none -o ControlPath=none \
|
||||
"$target" exit
|
||||
if test $status -ne 0
|
||||
echo "ff ssh: 公钥可能已复制,但所选密钥登录验证失败,未写入 Host。加密私钥请先用 ssh-add 解锁,或用 --no-copy 只写配置" >&2
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
# `ff ssh a@b.com` / `ff ssh -p 2222 -i key root@host`
|
||||
@@ -409,9 +460,11 @@ function __ff_ssh_add_host
|
||||
set -l alias_name $host
|
||||
if test -t 0
|
||||
read -P "SSH 别名 [$host]: " answer
|
||||
if test $status -eq 0; and test -n "$answer"
|
||||
set alias_name $answer
|
||||
if test $status -ne 0
|
||||
echo "ff ssh: 已取消,未复制公钥或改写配置" >&2
|
||||
return 1
|
||||
end
|
||||
test -n "$answer"; and set alias_name $answer
|
||||
end
|
||||
if not string match -qr '^[A-Za-z0-9][A-Za-z0-9._-]*$' -- $alias_name
|
||||
echo "ff ssh: 非法别名 '$alias_name'(只允许字母、数字、点、下划线、连字符)" >&2
|
||||
@@ -420,12 +473,35 @@ function __ff_ssh_add_host
|
||||
|
||||
set -l existing (__ff_ssh_find_alias_file $alias_name)
|
||||
if test -n "$existing"
|
||||
echo "SSH 别名 '$alias_name' 已存在于 $existing,跳过写入"
|
||||
return 0
|
||||
if test (count $existing) -ne 1
|
||||
echo "ff ssh: 别名 '$alias_name' 有多个 Host 声明($existing),请先手动合并;未复制公钥或改写配置" >&2
|
||||
return 1
|
||||
end
|
||||
echo "SSH 别名 '$alias_name' 已存在于 $existing"
|
||||
if not test -t 0
|
||||
echo "ff ssh: 非交互模式无法确认覆盖,未复制公钥或改写配置" >&2
|
||||
return 1
|
||||
end
|
||||
read -l -P "覆盖更新 '$alias_name'?[y/N]: " overwrite
|
||||
if test $status -ne 0
|
||||
echo "ff ssh: 已取消覆盖,未复制公钥或改写配置" >&2
|
||||
return 1
|
||||
end
|
||||
if not contains -- (string lower -- (string trim -- "$overwrite")) y yes
|
||||
echo "ff ssh: 未确认覆盖,未复制公钥或改写配置" >&2
|
||||
return 1
|
||||
end
|
||||
else if test -e "$HOME/.ssh/config.d/$alias_name.conf"
|
||||
echo "ff ssh: 目标文件已存在但未声明 '$alias_name',请先手动整理;未复制公钥或改写配置" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
# 用 ssh -G 解析最终生效的 HostName/User/Port/Proxy*
|
||||
set -l resolved (ssh -G $args 2>/dev/null)
|
||||
if test $status -ne 0
|
||||
echo "ff ssh: 无法解析 SSH 配置,未写入 Host" >&2
|
||||
return 1
|
||||
end
|
||||
set -l rhost (printf '%s\n' $resolved | __ff_ssh_gval hostname)
|
||||
set -l ruser (printf '%s\n' $resolved | __ff_ssh_gval user)
|
||||
set -l rport (printf '%s\n' $resolved | __ff_ssh_gval port)
|
||||
@@ -438,38 +514,87 @@ function __ff_ssh_add_host
|
||||
test "$proxyjump" = none; and set proxyjump ""
|
||||
test "$proxycommand" = none; and set proxycommand ""
|
||||
|
||||
# IdentityFile: ssh -G 会列出所有默认密钥,仅在显式指定时记录
|
||||
set -l identity ""
|
||||
set -l explicit_identity (__ff_ssh_argv_value -i $args)
|
||||
# Explicit -i / -o IdentityFile must never silently fall back to another key.
|
||||
set -l explicit_identity ""
|
||||
for i in (seq (count $args))
|
||||
if test "$args[$i]" = -i; and test $i -lt (count $args)
|
||||
set explicit_identity $args[(math $i + 1)]
|
||||
break
|
||||
else if string match -q -- '-i*' "$args[$i]"
|
||||
set explicit_identity (string sub -s 3 -- "$args[$i]")
|
||||
break
|
||||
end
|
||||
set -l option ""
|
||||
if test "$args[$i]" = -o; and test $i -lt (count $args)
|
||||
set option $args[(math $i + 1)]
|
||||
else if string match -q -- '-o*' "$args[$i]"
|
||||
set option (string sub -s 3 -- "$args[$i]")
|
||||
end
|
||||
if string match -qri '^identityfile[=[:space:]]' -- "$option"
|
||||
set explicit_identity (string replace -ri '^identityfile[=[:space:]]+' '' -- "$option")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
set -l identities (printf '%s\n' $resolved | string match -ri '^identityfile[[:space:]]+.*$' | string replace -r '^\S+\s+' '')
|
||||
set -l copy_identity ""
|
||||
set -l configured_identity 0
|
||||
if test -n "$explicit_identity"
|
||||
set identity $explicit_identity
|
||||
else if string match -q '*IdentityFile*' -- $args
|
||||
set identity (printf '%s\n' $resolved | __ff_ssh_gval identityfile)
|
||||
# ssh -G can omit an unreadable -i and print defaults instead.
|
||||
set copy_identity (__ff_ssh_private_path "$explicit_identity")
|
||||
set identities
|
||||
end
|
||||
if string match -q -- "$HOME/*" "$identity"
|
||||
set identity "~/"(string replace -r "^$HOME/" '' -- $identity)
|
||||
for candidate in $identities
|
||||
set -l private (__ff_ssh_private_path "$candidate")
|
||||
set -l name (string replace -- "$HOME/.ssh/" '' "$private")
|
||||
if not contains -- "$name" id_rsa id_dsa id_ecdsa id_ecdsa_sk id_ed25519 id_ed25519_sk id_xmss
|
||||
set configured_identity 1
|
||||
end
|
||||
if test -r "$private"; and test -r "$private.pub"
|
||||
set copy_identity "$private"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# ssh-copy-id 也要用同一把密钥(-i 或 -o IdentityFile=),否则会复制默认 key
|
||||
set -l copy_identity $explicit_identity
|
||||
if test -z "$copy_identity"; and test -n "$identity"
|
||||
set copy_identity (string replace -r '^~/' "$HOME/" -- $identity)
|
||||
# ssh-copy-id also supports nonstandard id_*.pub names. Among these, keep
|
||||
# its newest-public-key preference, but only select a complete readable pair.
|
||||
if test -z "$copy_identity"; and test -z "$explicit_identity"; and test "$configured_identity" = 0
|
||||
for pub in (command ls -t "$HOME"/.ssh/id_*.pub 2>/dev/null)
|
||||
string match -q '*-cert.pub' -- "$pub"; and continue
|
||||
set -l private (__ff_ssh_private_path "$pub")
|
||||
if test -r "$private"; and test -r "$pub"
|
||||
set copy_identity "$private"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 先复制公钥,这样写完配置就能直接 ssh
|
||||
if test "$do_copy" = 1
|
||||
__ff_ssh_ensure_keys
|
||||
__ff_ssh_copy_id "$target" "$port" "$copy_identity" "$proxyjump" "$proxycommand"
|
||||
or echo "ff ssh: 公钥复制失败,已继续写入配置(可重试: ff ssh add $target)" >&2
|
||||
if test -z "$copy_identity"; and test -z "$explicit_identity"; and test "$configured_identity" = 0
|
||||
__ff_ssh_ensure_keys; or return 1
|
||||
set -l generated "$HOME/.ssh/id_ed25519"
|
||||
if test -r "$generated"; and test -r "$generated.pub"
|
||||
set copy_identity "$generated"
|
||||
end
|
||||
end
|
||||
if test -z "$copy_identity"; or not test -r "$copy_identity"; or not test -r "$copy_identity.pub"
|
||||
echo "ff ssh: 未找到可读的公私钥对,请用 -i 指定密钥;未写入 Host" >&2
|
||||
return 1
|
||||
end
|
||||
echo "所选 SSH 密钥: $copy_identity"
|
||||
set -l copy_target "$rhost"
|
||||
test -n "$user"; and set copy_target "$user@$rhost"
|
||||
__ff_ssh_copy_id "$copy_target" "$port" "$copy_identity" "$proxyjump" "$proxycommand"; or return 1
|
||||
end
|
||||
|
||||
set -l identity "$copy_identity"
|
||||
if string match -q -- "$HOME/*" "$identity"
|
||||
set identity "~/"(string sub -s (math (string length -- "$HOME") + 2) -- "$identity")
|
||||
end
|
||||
|
||||
set -l dir "$HOME/.ssh/config.d"
|
||||
mkdir -p $dir
|
||||
chmod 700 "$HOME/.ssh" $dir
|
||||
set -l conf_file "$dir/$alias_name.conf"
|
||||
if test -f $conf_file
|
||||
cp -p $conf_file "$conf_file.backup."(date +%Y%m%d%H%M%S)
|
||||
end
|
||||
test -n "$existing"; and set conf_file "$existing"
|
||||
|
||||
set -l tmpdir /tmp
|
||||
test -n "$TMPDIR"; and set tmpdir $TMPDIR
|
||||
@@ -482,7 +607,7 @@ function __ff_ssh_add_host
|
||||
end
|
||||
printf ' Port %s\n' $port
|
||||
if test -n "$identity"
|
||||
printf ' IdentityFile %s\n' $identity
|
||||
printf ' IdentityFile "%s"\n' $identity
|
||||
end
|
||||
if test -n "$proxyjump"
|
||||
printf ' ProxyJump %s\n' $proxyjump
|
||||
@@ -491,8 +616,33 @@ function __ff_ssh_add_host
|
||||
printf ' ProxyCommand %s\n' $proxycommand
|
||||
end
|
||||
end > $tmp
|
||||
if test -n "$existing"
|
||||
set -l updated (mktemp "$tmpdir/ff-ssh.XXXXXX"); or begin
|
||||
rm -f $tmp
|
||||
return 1
|
||||
end
|
||||
__ff_ssh_replace_alias "$alias_name" "$conf_file" "$tmp" > $updated; or begin
|
||||
rm -f $tmp $updated
|
||||
return 1
|
||||
end
|
||||
rm -f $tmp
|
||||
set tmp $updated
|
||||
# config.d/* would load backups too, so keep them outside that directory.
|
||||
set -l backup_dir "$HOME/.ssh/config.backups"
|
||||
mkdir -p "$backup_dir"; or return 1
|
||||
chmod 700 "$backup_dir"
|
||||
set -l backup (mktemp "$backup_dir/"(basename "$conf_file")".backup.XXXXXX"); or return 1
|
||||
cp -p "$conf_file" "$backup"; or begin
|
||||
rm -f $tmp $backup
|
||||
return 1
|
||||
end
|
||||
echo "原配置备份: $backup"
|
||||
end
|
||||
mkdir -p $dir; or return 1
|
||||
chmod 700 "$HOME/.ssh" $dir
|
||||
chmod 600 $tmp
|
||||
mv $tmp $conf_file
|
||||
mv $tmp $conf_file; or return 1
|
||||
__ff_ssh_ensure_config_d; or return 1
|
||||
|
||||
echo "已写入 SSH 配置: $conf_file"
|
||||
echo "以后可直接运行: ssh $alias_name"
|
||||
@@ -509,7 +659,10 @@ function __ff_ssh_usage
|
||||
echo " help 显示此帮助"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " --no-copy 只写配置,不执行 ssh-copy-id"
|
||||
echo " --no-copy 只写配置,不执行 ssh-copy-id 或登录验证"
|
||||
echo " 重复别名需交互确认覆盖(默认否),成功后备份并在原位置更新"
|
||||
echo " 公钥复制后仅用所选密钥验证登录;连接超时 10 秒,失败或取消不写 Host"
|
||||
echo " 验证使用 BatchMode;加密私钥请先用 ssh-add 解锁"
|
||||
echo ""
|
||||
echo "保留子命令: init / copy / add / run / help(同名主机请用 ssh 直接连接)"
|
||||
echo ""
|
||||
@@ -530,7 +683,6 @@ function ff_ssh
|
||||
__ff_ssh_ensure_keys
|
||||
__ff_ssh_ensure_config_d
|
||||
case add
|
||||
__ff_ssh_ensure_config_d
|
||||
__ff_ssh_add_host $argv[2..-1]
|
||||
case run
|
||||
set -l run_args
|
||||
@@ -538,7 +690,6 @@ function ff_ssh
|
||||
test "$a" = --no-copy; and continue
|
||||
set run_args $run_args $a
|
||||
end
|
||||
__ff_ssh_ensure_config_d
|
||||
__ff_ssh_add_host $argv[2..-1]; or return 1
|
||||
command ssh $run_args
|
||||
case copy
|
||||
@@ -548,7 +699,6 @@ function ff_ssh
|
||||
case '*'
|
||||
# 裸参数与 ssh 原生参数一致:有主机就记录配置,否则透传给 ssh
|
||||
if __ff_ssh_find_target $argv >/dev/null
|
||||
__ff_ssh_ensure_config_d
|
||||
__ff_ssh_add_host $argv
|
||||
else
|
||||
command ssh $argv
|
||||
|
||||
Reference in New Issue
Block a user