ssh bug fix
This commit is contained in:
@@ -138,14 +138,14 @@ A bar-style prompt showing current time, execution time of the last command
|
|||||||
| `pwdscore` | pwd | Calculate the strength score of a given password |
|
| `pwdscore` | pwd | Calculate the strength score of a given password |
|
||||||
| `rain` | rain | Console screensaver with falling-rain effect (multiple color themes) |
|
| `rain` | rain | Console screensaver with falling-rain effect (multiple color themes) |
|
||||||
| `rainbow` | rain | Full-screen rainbow screensaver using only background colors, with horizontal color shifting |
|
| `rainbow` | rain | Full-screen rainbow screensaver using only background colors, with horizontal color shifting |
|
||||||
| `rmhost` | ssh | Remove host(s) (name and IP) from SSH known_hosts; supports `--all-users` as root |
|
| `rmhost` | ssh | Remove host(s) from SSH known_hosts: a name is removed with every IP address it resolves to, `[host]:port` entries and glob patterns are supported; `--all-users` (root) handles every local user's file and keeps its owner and mode |
|
||||||
| `rmspc` | filefct | Replace spaces in filenames with underscores (or a custom character, attached: `-c-` / `--subst-char=-`); never overwrites an existing file and does not follow symlinked directories |
|
| `rmspc` | filefct | Replace spaces in filenames with underscores (or a custom character, attached: `-c-` / `--subst-char=-`); never overwrites an existing file and does not follow symlinked directories |
|
||||||
| `setc` | lang | Set locale to standard C (POSIX) |
|
| `setc` | lang | Set locale to standard C (POSIX) |
|
||||||
| `setlocale` | lang | Set console locale to any installed locale (codeset spelling tolerant, UTF-8 preferred when none is given); clears `LC_ALL` and the other per-category variables so the change always applies |
|
| `setlocale` | lang | Set console locale to any installed locale (codeset spelling tolerant, UTF-8 preferred when none is given); clears `LC_ALL` and the other per-category variables so the change always applies |
|
||||||
| `set_theme` | prompt | Switch the prompt colour theme for the session (`--list`, `--preview`, `--save`) |
|
| `set_theme` | prompt | Switch the prompt colour theme for the session (`--list`, `--preview`, `--save`) |
|
||||||
| `settrace` | debug | Activate (`--on`), deactivate (`--off`) or query (`--status`) an ERR trap that prints the exit status and a call stack (on stderr) for each failing command; `--force` replaces/removes an ERR trap set by something else; active at startup when `SETTRACE_DEFAULT_ENABLED=1` |
|
| `settrace` | debug | Activate (`--on`), deactivate (`--off`) or query (`--status`) an ERR trap that prints the exit status and a call stack (on stderr) for each failing command; `--force` replaces/removes an ERR trap set by something else; active at startup when `SETTRACE_DEFAULT_ENABLED=1` |
|
||||||
| `showinfo` | info | Display welcome banner and system information (figlet + fastfetch/neofetch) |
|
| `showinfo` | info | Display welcome banner and system information (figlet + fastfetch/neofetch) |
|
||||||
| `ssr` | ssh | SSH into a server as root, forwarding extra ssh options |
|
| `ssr` | ssh | SSH into a server as root (the server comes first, then extra ssh options); default options from `SSH_DEFAULT_OPT` |
|
||||||
| `taz` | compress | Compress files and directories into a chosen archive format |
|
| `taz` | compress | Compress files and directories into a chosen archive format |
|
||||||
| `term_set` | conf | Set `TERM` to the best available terminal capability |
|
| `term_set` | conf | Set `TERM` to the best available terminal capability |
|
||||||
| `urlencode` | net | URL-encode a string |
|
| `urlencode` | net | URL-encode a string |
|
||||||
@@ -261,7 +261,7 @@ change the default without having to pass flags every time.
|
|||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `SSH_DEFAULT_OPT` | _(empty)_ | Extra options passed to `ssr` (e.g. `-Y` for X forwarding) |
|
| `SSH_DEFAULT_OPT` | `-Y` when unset | Options passed to `ssr` before the server (e.g. `-Y -o StrictHostKeyChecking=accept-new`); set it empty for none |
|
||||||
|
|
||||||
**`[pwd]`** — password tools
|
**`[pwd]`** — password tools
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,15 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
silent white, a null speed no longer spins the CPU, a non-terminal stdin or
|
silent white, a null speed no longer spins the CPU, a non-terminal stdin or
|
||||||
stdout is refused instead of looping, and the usage mentions `q` to quit;
|
stdout is refused instead of looping, and the usage mentions `q` to quit;
|
||||||
README defaults of the `[rain]` keys corrected.
|
README defaults of the `[rain]` keys corrected.
|
||||||
|
- `ssh.sh`: `rmhost` with a wildcard that matched nothing tried to remove an
|
||||||
|
empty host name; it removed only the first IP of a name (all addresses are
|
||||||
|
now handled); `[host]:port` arguments were taken for glob patterns; each
|
||||||
|
match was removed from every file with warnings for the others (now per
|
||||||
|
file); with `--all-users`, `ssh-keygen` left other users' files owned by
|
||||||
|
root (owner and mode are restored) and a shared home directory was
|
||||||
|
processed twice; `@cert-authority` lines are never touched. `ssr` ignored an
|
||||||
|
empty `SSH_DEFAULT_OPT` (documented as "no option") and now rejects an
|
||||||
|
option given in place of the server.
|
||||||
- `compress.sh`: option flags and loop variables no longer leak into the
|
- `compress.sh`: option flags and loop variables no longer leak into the
|
||||||
interactive shell.
|
interactive shell.
|
||||||
|
|
||||||
|
|||||||
+106
-49
@@ -36,6 +36,35 @@
|
|||||||
|
|
||||||
load_conf "ssh"
|
load_conf "ssh"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Internal helper: remove <key> (a name or an IP, in known_hosts notation, so
|
||||||
|
# also "[host]:port") from the known_hosts file <file> with ssh-keygen -R.
|
||||||
|
# ssh-keygen writes a new file: run by root on another user's file (--all-users)
|
||||||
|
# it would leave it owned by root with mode 0644, which keeps that user's ssh
|
||||||
|
# from adding host keys, or exposes a private file. The owner and mode of the
|
||||||
|
# file, and of its ".old" backup, are therefore put back.
|
||||||
|
# Returns ssh-keygen's status (non-zero when the key was not found).
|
||||||
|
# Usage: _rmhost_remove <key> <file>
|
||||||
|
_rmhost_remove()
|
||||||
|
{
|
||||||
|
local key="$1" file="$2" owner="" mode="" rc=0
|
||||||
|
if [[ ${EUID:-$(id -u)} -eq 0 ]]; then
|
||||||
|
owner=$(stat -c '%u:%g' -- "$file" 2>/dev/null)
|
||||||
|
mode=$(stat -c '%a' -- "$file" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh-keygen -R "$key" -f "$file" </dev/null >/dev/null 2>&1 || rc=$?
|
||||||
|
|
||||||
|
if [[ -n $owner && -n $mode ]]; then
|
||||||
|
chown "$owner" -- "$file" "${file}.old" 2>/dev/null
|
||||||
|
chmod "$mode" -- "$file" "${file}.old" 2>/dev/null
|
||||||
|
fi
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
|
export -f _rmhost_remove
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Remove host entries (name and IP) from ~/.ssh/known_hosts for the active user
|
# Remove host entries (name and IP) from ~/.ssh/known_hosts for the active user
|
||||||
# Usage: rmhost <hostname|ip> [hostname2|ip2 [...]]
|
# Usage: rmhost <hostname|ip> [hostname2|ip2 [...]]
|
||||||
@@ -47,10 +76,12 @@ rmhost()
|
|||||||
|
|
||||||
PARSED=$(getopt -o ha --long help,all-users -n 'rmhost' -- "$@")
|
PARSED=$(getopt -o ha --long help,all-users -n 'rmhost' -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [[ $? -ne 0 ]]; then return 1; fi
|
if [[ $? -ne 0 ]]; then
|
||||||
local -a _pargs=()
|
disp E "Invalid options, use \"rmhost --help\" to display usage."
|
||||||
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
|
return 1
|
||||||
set -- "${_pargs[@]}"
|
fi
|
||||||
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
eval set -- "$PARSED"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -60,6 +91,7 @@ rmhost()
|
|||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf " -a, --all-users Remove entries from all local users when run as root\n"
|
printf " -a, --all-users Remove entries from all local users when run as root\n"
|
||||||
printf " -h, --help Display this help screen\n\n"
|
printf " -h, --help Display this help screen\n\n"
|
||||||
|
printf "A host name is removed together with all the IP addresses it resolves to.\n"
|
||||||
printf "Wildcards:\n"
|
printf "Wildcards:\n"
|
||||||
printf " Glob patterns (*, ?, [...]) are expanded against unhashed known_hosts entries.\n"
|
printf " Glob patterns (*, ?, [...]) are expanded against unhashed known_hosts entries.\n"
|
||||||
printf " Hashed entries (prefixed with |1|) are never matched by wildcards.\n"
|
printf " Hashed entries (prefixed with |1|) are never matched by wildcards.\n"
|
||||||
@@ -90,14 +122,27 @@ rmhost()
|
|||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A name starting with "-" would be read as an option by the tools below
|
||||||
|
local target
|
||||||
|
for target in "$@"; do
|
||||||
|
if [[ "$target" == -* ]]; then
|
||||||
|
disp E "Invalid host '$target'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
if (( all_users )); then
|
if (( all_users )); then
|
||||||
[[ ${EUID:-$(id -u)} -eq 0 ]] || {
|
[[ ${EUID:-$(id -u)} -eq 0 ]] || {
|
||||||
disp E "Option --all-users is only available when run as root."
|
disp E "Option --all-users is only available when run as root."
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Users may share a home directory: each file is listed once
|
||||||
|
local -A _seen_homes=()
|
||||||
|
local home
|
||||||
while IFS=: read -r _ _ _ _ _ home _; do
|
while IFS=: read -r _ _ _ _ _ home _; do
|
||||||
[[ -n $home && -f $home/.ssh/known_hosts ]] || continue
|
[[ -n $home && -f $home/.ssh/known_hosts && -z ${_seen_homes[$home]+x} ]] || continue
|
||||||
|
_seen_homes[$home]=1
|
||||||
known_hosts_files+=("$home/.ssh/known_hosts")
|
known_hosts_files+=("$home/.ssh/known_hosts")
|
||||||
done < /etc/passwd
|
done < /etc/passwd
|
||||||
|
|
||||||
@@ -109,19 +154,27 @@ rmhost()
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
if [[ ! -f "${HOME}/.ssh/known_hosts" ]]; then
|
||||||
|
disp W "No known_hosts file found (${HOME}/.ssh/known_hosts)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
known_hosts_files=("${HOME}/.ssh/known_hosts")
|
known_hosts_files=("${HOME}/.ssh/known_hosts")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for target in "$@"; do
|
for target in "$@"; do
|
||||||
# Wildcard: expand glob pattern against unhashed known_hosts entries
|
# Wildcard: expand the glob pattern against the unhashed entries of
|
||||||
if [[ "$target" == *['*?[']* ]]; then
|
# each file, and remove the matches from that file only. A "[host]:port"
|
||||||
local -a _matched=()
|
# entry is not a pattern: its brackets are known_hosts notation.
|
||||||
local _wf _wl _wfield _whost _wmatch
|
if [[ "$target" == *['*?']* ]] || \
|
||||||
local -a _wentries
|
[[ "$target" == *'['* && ! "$target" =~ ^\[[^][]+\]:[0-9]+$ ]]; then
|
||||||
|
local -a _matched=() _wentries=()
|
||||||
|
local _wf _wl _wfield _whost _wmatch _key _found=0
|
||||||
for _wf in "${known_hosts_files[@]}"; do
|
for _wf in "${known_hosts_files[@]}"; do
|
||||||
[[ -f "$_wf" ]] || continue
|
_matched=()
|
||||||
while IFS= read -r _wl; do
|
while IFS= read -r _wl; do
|
||||||
[[ -z "$_wl" || "$_wl" == '#'* || "$_wl" == '|'* ]] && continue
|
# Comments, hashed entries and @cert-authority/@revoked
|
||||||
|
# marker lines are left alone
|
||||||
|
[[ -z "$_wl" || "$_wl" == '#'* || "$_wl" == '|'* || "$_wl" == '@'* ]] && continue
|
||||||
_wfield="${_wl%% *}"
|
_wfield="${_wl%% *}"
|
||||||
IFS=',' read -ra _wentries <<< "$_wfield"
|
IFS=',' read -ra _wentries <<< "$_wfield"
|
||||||
for _whost in "${_wentries[@]}"; do
|
for _whost in "${_wentries[@]}"; do
|
||||||
@@ -132,55 +185,50 @@ rmhost()
|
|||||||
else
|
else
|
||||||
_wmatch="$_whost"
|
_wmatch="$_whost"
|
||||||
fi
|
fi
|
||||||
# shellcheck disable=SC2053
|
# shellcheck disable=SC2053 # unquoted on purpose: glob match
|
||||||
[[ "$_wmatch" == $target ]] && _matched+=("$_whost")
|
[[ "$_wmatch" == $target ]] && _matched+=("$_whost")
|
||||||
done
|
done
|
||||||
done < "$_wf"
|
done < "$_wf"
|
||||||
done
|
|
||||||
|
|
||||||
mapfile -t _matched < <(printf '%s\n' "${_matched[@]}" | sort -u)
|
[[ ${#_matched[@]} -gt 0 ]] || continue
|
||||||
|
_found=1
|
||||||
if [[ ${#_matched[@]} -eq 0 ]]; then
|
while IFS= read -r _key; do
|
||||||
disp W "No known_hosts entries match pattern '$target'."
|
disp I "Removing '$_key' from $_wf..."
|
||||||
continue
|
if ! _rmhost_remove "$_key" "$_wf"; then
|
||||||
fi
|
disp W "No known_hosts entry found for '$_key' in '$_wf'."
|
||||||
|
|
||||||
local _key _known_hosts_file
|
|
||||||
for _key in "${_matched[@]}"; do
|
|
||||||
for _known_hosts_file in "${known_hosts_files[@]}"; do
|
|
||||||
disp I "Removing '$_key' from $_known_hosts_file..."
|
|
||||||
if ! ssh-keygen -R "$_key" -f "$_known_hosts_file" >/dev/null 2>&1; then
|
|
||||||
disp W "No known_hosts entry found for '$_key' in '$_known_hosts_file'."
|
|
||||||
fi
|
fi
|
||||||
done
|
done < <(printf '%s\n' "${_matched[@]}" | sort -u)
|
||||||
done
|
done
|
||||||
|
|
||||||
|
(( _found )) || disp W "No known_hosts entries match pattern '$target'."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local hst="$target"
|
local hst="$target"
|
||||||
local ip=""
|
local ip
|
||||||
local v4=1
|
local -a ips=()
|
||||||
local v6=1
|
|
||||||
|
|
||||||
isipv4 "$hst" >/dev/null 2>&1; v4=$?
|
if isipv4 "$hst" >/dev/null 2>&1 || isipv6 "$hst" >/dev/null 2>&1; then
|
||||||
isipv6 "$hst" >/dev/null 2>&1; v6=$?
|
ips=("$hst")
|
||||||
|
|
||||||
if [[ $v4 -eq 0 || $v6 -eq 0 ]]; then
|
|
||||||
ip="$hst"
|
|
||||||
hst=""
|
hst=""
|
||||||
fi
|
elif [[ "$hst" == '['*']:'* ]]; then
|
||||||
|
# Explicit [host]:port entry: removed exactly as given
|
||||||
if [[ -z ${ip:-} && -n ${hst:-} ]]; then
|
:
|
||||||
|
else
|
||||||
|
# All the addresses of the name: ssh records the one it connected
|
||||||
|
# to, which may be any of them (IPv4 and IPv6, several A records)
|
||||||
if command -v host >/dev/null 2>&1; then
|
if command -v host >/dev/null 2>&1; then
|
||||||
ip=$(host "$hst" 2>/dev/null |
|
mapfile -t ips < <(host "$hst" 2>/dev/null |
|
||||||
awk '/has address|has IPv6 address/ {print $NF; exit}')
|
awk '/has address|has IPv6 address/ {print $NF}' | sort -u)
|
||||||
elif command -v getent >/dev/null 2>&1; then
|
elif command -v getent >/dev/null 2>&1; then
|
||||||
ip=$(getent ahosts "$hst" 2>/dev/null | awk 'NR == 1 {print $1; exit}')
|
# ahosts lists every address once per socket type
|
||||||
|
mapfile -t ips < <(getent ahosts "$hst" 2>/dev/null |
|
||||||
|
awk '{print $1}' | sort -u)
|
||||||
else
|
else
|
||||||
disp W "No resolver tool found; removing hostname only for '$hst'."
|
disp W "No resolver tool found; removing hostname only for '$hst'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -z ${ip:-} ]] && \
|
[[ ${#ips[@]} -eq 0 ]] && \
|
||||||
disp W "Could not resolve IP for '$hst'; removing hostname only."
|
disp W "Could not resolve IP for '$hst'; removing hostname only."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -188,16 +236,16 @@ rmhost()
|
|||||||
for known_hosts_file in "${known_hosts_files[@]}"; do
|
for known_hosts_file in "${known_hosts_files[@]}"; do
|
||||||
if [[ -n ${hst:-} ]]; then
|
if [[ -n ${hst:-} ]]; then
|
||||||
disp I "Removing host $hst from $known_hosts_file..."
|
disp I "Removing host $hst from $known_hosts_file..."
|
||||||
if ! ssh-keygen -R "$hst" -f "$known_hosts_file" >/dev/null 2>&1; then
|
if ! _rmhost_remove "$hst" "$known_hosts_file"; then
|
||||||
disp W "No known_hosts entry found for '$hst' in '$known_hosts_file'."
|
disp W "No known_hosts entry found for '$hst' in '$known_hosts_file'."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -n ${ip:-} ]]; then
|
for ip in "${ips[@]}"; do
|
||||||
disp I "Removing IP $ip from $known_hosts_file..."
|
disp I "Removing IP $ip from $known_hosts_file..."
|
||||||
if ! ssh-keygen -R "$ip" -f "$known_hosts_file" >/dev/null 2>&1; then
|
if ! _rmhost_remove "$ip" "$known_hosts_file"; then
|
||||||
disp W "No known_hosts entry found for '$ip' in '$known_hosts_file'."
|
disp W "No known_hosts entry found for '$ip' in '$known_hosts_file'."
|
||||||
fi
|
fi
|
||||||
fi
|
done
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -238,9 +286,18 @@ ssr()
|
|||||||
local srv=$1
|
local srv=$1
|
||||||
shift
|
shift
|
||||||
|
|
||||||
# Build default options array from config, falling back to -Y (X11 forwarding)
|
# The first argument is the server: an option here ("ssr -p 22 host")
|
||||||
|
# would end up in the root@... destination
|
||||||
|
if [[ $srv == -* ]]; then
|
||||||
|
disp E "'$srv' is not a server name: the server comes first, then the ssh options."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the default options array from the configuration. An unset
|
||||||
|
# SSH_DEFAULT_OPT means -Y (X11 forwarding); an empty one means no default
|
||||||
|
# option at all, as the example configuration documents.
|
||||||
local -a ssh_default_opts=()
|
local -a ssh_default_opts=()
|
||||||
if [[ -n "${SSH_DEFAULT_OPT:-}" ]]; then
|
if [[ -n "${SSH_DEFAULT_OPT+x}" ]]; then
|
||||||
read -ra ssh_default_opts <<< "${SSH_DEFAULT_OPT}"
|
read -ra ssh_default_opts <<< "${SSH_DEFAULT_OPT}"
|
||||||
else
|
else
|
||||||
ssh_default_opts=(-Y)
|
ssh_default_opts=(-Y)
|
||||||
|
|||||||
Reference in New Issue
Block a user