effect scp -O (use legacy SCP procotol) per host in .ssh/config?

Phil Pennock phil.pennock at globnix.org
Thu Jun 23 10:07:01 AEST 2022


On 2022-06-22 at 10:48 +0200, Jochen Bern wrote:
> Now if only we could get the hostname(s) without entirely duplicating scp's
> command line parsing ....... ?

I have this, because I wrap both ssh and scp to be able to easily
redirect the agent socket based on a profile, to separate work from
personal keys and "code" from "prod".  Some stuff is configured via
direnv setting of the right env var, and some stuff by a sourced config
file which can set override variables.  It's handy but very
personal-setup-specific.

On the other hand, when trying to strip this down to the core parts
right now I decided I was too likely to introduce bugs.  So you know
what?  Have two scripts which grew a bit, will make people scream, and
let you keep all the broken fragments when it all goes horribly wrong.

For the "set the -O on some hosts" purpose you should ignore the
SSH_PDP_ROLE bits; that's for integration into my ssh-role script which
manages the systemd launch from a template ssh-agent-pdp at .service to
coordinate things.  It shouldn't be needed.  That stuff ... grew
organically.  I'm not including that script or the .service file.

I don't expect anyone to use these as-is, but ... it's all been debugged
over a few years and has the worst rough edges worn smooth with only a
little bit of my blood on them, so it's not a _horrible_ starting point.
There's probably quite a few places left where there are bugs but this
has worked enough to let me work day-to-day without usually noticing
that there's a wrapper in the way.

-Phil
-------------- next part --------------
#!/usr/bin/env -S zsh -feu
set -eu

# Our override env vars:
#  * SSH_PDP_COMMAND: ssh command or pathname to use
#  * SSH_PDP_BYPASS: if set and non-empty, then ignore all other logic except SSH_PDP_COMMAND
#  * SSH_PDP_ROLE: per-role SSH agent support; identifier
#  * SSH_PDP_ROLEFILE: location of non-default rolefile of keys
#  * SSH_PDP_GOTO: used by my goto system, disables escape char
#  * SSH_PDP_TRACE: used for some extra debug info
#  * SSH_PDP_SKIP_TITLE: set non-empty to inhibit terminal manipulations
# Our extra config files:
#  * ~/.ssh/role.<agentname> (or $SSH_PDP_ROLEFILE): ssh-keys to load
#  * ~/.ssh/agent-keyuse-confirm: existence prompts -c for ssh-add, same as ssh-add-today
#  * ~/etc/site/ssh.hosts-mapping: sourced, can change terminal profile names etc; also set the Role

progname="${0:A:t}"
warn() { printf >&2 '%s: %s\n' "$progname" "$*"; }
die() { warn "$@"; exit 1; }
have_cmd() { (( $+commands[$1] )) }

readonly DEFAULT_KEY_DURATION='20h'

# Finding the actual comand to call.
#
case "${SSH_PDP_COMMAND:-ssh}" in
	/*) command="${SSH_PDP_COMMAND:-ssh}" ;;
	*) # minimize diff, leave this block unindented
saved_PATH="$PATH"
typeset -gU path
#  We bias the result, for platforms where we need to keep system bins in path
# first but we want a newer SSH
path=( /opt/openssh/bin /opt/local/bin /usr/local/opt/openssh/bin /usr/local/bin "${path[@]}" )
# We drop our own directory from path; -U makes it unique, only need to drop once
path[(r)${0:h}]=()
#
command="$commands[${SSH_PDP_COMMAND:-ssh}]"
# We might use other utilities of our own below
PATH="$saved_PATH"
	;;
esac

if [[ -n "${SSH_PDP_BYPASS:-}" ]]; then
	exec "$command" "$@"
fi

# Per-role dedicated SSH Agent support, so can have keys per role
# We define the functions here, but we only call them after sourcing the user
# mapping file, so that roles can be set per-hostname.
#
set_agent_socket_systemd() {
	if [[ "$SSH_PDP_ROLE" == "default" ]]; then
		want_socket="$XDG_RUNTIME_DIR/pdp.ssh/agent.systemd"
		export SSH_AUTH_SOCK="$want_socket"
		return
	fi
	want_socket="$XDG_RUNTIME_DIR/pdp.ssh/agent-${SSH_PDP_ROLE}.systemd"
	if [[ ! -S "$want_socket" ]]; then
		warn "Asking systemd to start ${(q-)SSH_PDP_ROLE} socket"
		systemctl --user start "ssh-agent-pdp@$SSH_PDP_ROLE"
		[[ -S "$want_socket" ]] || die "missing ${(q-)want_socket} after systemd user start"
	fi
	export SSH_AUTH_SOCK="$want_socket"
}
set_agent_socket() {
	[[ -n "${SSH_PDP_ROLE:-}" ]] || return 0
	case "$SSH_PDP_ROLE" in
	(*/* | *$'\0'* ) die "malformed SSH_PDP_ROLE" ;;
	esac

	if [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then
		if have_cmd systemctl; then
			set_agent_socket_systemd
			return $?
		fi
		warn "XDG-using platform, no systemctl"
	else
		warn "non-XDG platform"
	fi

	warn "unable to setup per-role ssh-agent for ${(q-)SSH_PDP_ROLE} on this platform"
	return 1
}
load_per_agent_keys() {
	[[ -n "${SSH_PDP_ROLE:-}" ]] || return 0
	[[ "$SSH_PDP_ROLE" != "default" ]] || return 0
	if ssh-add -l >/dev/null 2>&1; then return 0; fi

	local rolefile="${SSH_PDP_ROLEFILE:-$HOME/.ssh/role.$SSH_PDP_ROLE}"
	if [[ ! -f "$rolefile" ]]; then
		warn "Missing ${(q-)rolefile}, not auto-loading keys for ${(q-)SSH_PDP_ROLE}"
		return 1
	fi
	local -a key_files load_params
	load_params=(-t "$DEFAULT_KEY_DURATION")
	if [[ -e "$HOME/.ssh/agent-keyuse-confirm" ]]; then
		load_params+=(-c)
	fi
	local spec keydir
	keydir="${rolefile:A:h}"
	while read -r line; do
		case "${line:-empty}" in
		(empty) ;;
		(\#*) ;;
		(*)
			spec="${line%%\#*}"
			spec="${spec%[[:space:]]##}"
			spec="${spec#[[:space:]]##}"
			# match relevant entries from ssh_config(5) TOKENS
			spec="${spec//%d/$HOME}"
			spec="${spec//%u/$USERNAME}"
			spec="${spec//%%/%}"
			if [[ "$spec" == */* ]]; then
				spec="${spec/#~\//$HOME/}"
				spec="${spec/#.\//$keydir/}"
			fi
			key_files+=( "$spec" )
			;;
		esac
	done < "$rolefile"
	(( ${#key_files} )) || die "parsed no keys from ${(q-)rolefile}"
	ssh-add "${load_params[@]}" "${key_files[@]}" || die "ssh-add failed"
}

# We want $color_variant to be available as a Light/Dark switch for other stuff to auto-use
case "${COLORFGBG:-}" in
( 7\;0 | white\;black )
  reset_color_scheme='Dark Background'
  color_variant=Dark
  ;;
( 0\;7 | black\;white )
  reset_color_scheme='Light Background'
  color_variant=Light
  ;;
(*)
  reset_color_scheme='Dark Background'
  color_variant=Dark
  ;;
esac

# tab_coloring is for window tabs; color_schemes is for preset color schemes
typeset -A tab_coloring color_schemes
tab_coloring[example]=0:0:255
color_schemes[example]=Molokai


typeset -a ssh_invoke
ssh_invoke=( "$command" )
declare -i invoke_ssh_command_index=1
if [[ -n "${SSH_PDP_GOTO:-}" ]]; then
	ssh_invoke+=( -e none )
fi

if [[ $OSTYPE == darwin* ]]
then
	if (( ${${OSTYPE#darwin}%.*} >= 12.0 ))
	then
		ssh_invoke=(caffeinate -s "${ssh_invoke[@]}")
		invoke_ssh_command_index+=2
	fi

	# Keep this here, commented out, because the need _will_ return: {{{
	# Although: now should probably do this in the hosts-mapping by setting ssh_want_keepalive?
	#case $(wifi_ssid) in
	#(a|b|c-*|d*)
	#	ssh_invoke+=(-o ServerAliveInterval=120)
	#	;;
	#esac
	# }}}

	# Locking screen, too often no keys in agent
	if ! ssh-add -l > /dev/null 2>&1
	then
		print -u 2 "No SSH keys loaded into agent ..."
		ssh-add-today || exit 1
	fi
fi

if [[ -z "${TZ:-}" && -L /etc/localtime ]]
then
	zmodload -F zsh/stat b:zstat
	local h
	zstat -H h -L /etc/localtime
	export TZ=${h[link]##*/zoneinfo/}
fi

if [[ -n "${ITERM_SESSION_ID:-}" ]]; then
	# Beware: `Disable potentially insecure escape sequences` advanced setting (On by default) inhibits this
	terminal_profile() { printf >/dev/tty '\e]1337;SetProfile=%s\a' "${1:-Default}" ; }

	# Ripped from my iterm_tabcolor_rgb shell function
	tab_color() {
		case ${1:-.} in
		(off|disable|reset|clear) printf '\e]6;1;bg;*;default\a' > /dev/tty
			return ;;
		esac
		(( red   = ${1:?need a red 0-255 decimal} ))
		(( green = ${2:?need a green 0-255 decimal} ))
		(( blue  = ${3:?need a blue 0-255 decimal} ))
		printf '\e]6;1;bg;red;brightness;%d\a\e]6;1;bg;green;brightness;%d\a\e]6;1;bg;blue;brightness;%d\a' $red $green $blue > /dev/tty
	}

	terminal_color_scheme() {
		printf '\e]1337;SetColors=preset=%s\a' "$1" > /dev/tty
	}
	set_tab_title() { print >/dev/tty -n -- "\e]1;$*\a"; }
else
	terminal_profile() { : ; }
	tab_color() { : ; }
	terminal_color_scheme() { : ; }
	set_tab_title() { print >/dev/tty -n -- "\e]2;$*\a"; }
fi
# Saving the current window title is not portable, especially when OSC
# retrieval injects into keyboard stream so gets disabled as a security
# measure.
# So we just have set_tab_title().
# But while iTerm uses the icon title for the bit I want set, gnome-terminal
# ignores that and only displays the window title in tabs.  I could use 0 for
# both, but I have a vague recollection that on macOS, I didn't like the
# result, so instead just switch based on iTerm above.


# This is the variable set by lookup logic to say we're in an Interesting
# environment and the display should be mutated.
remote_env=''
# and this one for just "set the title but no other mutation"
remote_set_title=false
# this allows the title to be overridden, if desired
remote_title=''
# this/these add to ssh_invoke
ssh_want_keepalive=''  # number for interval, or just 'true'
# used to not set title when "ssh host <something>" by default, but perhaps we
# want that anyway, in which case override this.
ssh_set_title_even_if_remote_cmd=false
# This is used to let the config file override the ssh command.
# Should probably _not_ do so if SSH_PDP_COMMAND is set.
# This supports the idea of dispatching to an insecure ssh which still speaks
# some bad crypto variant, or a bug-compatible client, or a special binary with
# PQ support if testing that out with a particular host.
force_ssh_command=''

# We don't consume getopts, so don't shift by OPTIND, we just want to find the remote hostname
# We also find the remote_user, so ssh mapping can augment decisions about which role might be needed
# Last confirmed up-to-date: OpenSSH 8.6p1
remote_user=''
remote_hostname=''
remote_port=''
hostname_at_argc=-1
hostname_argv_prefix=''
hostname_argv_suffix=''
verbose=false
# This getopts string comes from the getopt() invocation in ssh.c, last updated 2022-04-17
while getopts '1246ab:c:e:fgi:kl:m:no:p:qstvxAB:CD:E:F:GI:J:KL:MNO:PQ:R:S:TVw:W:XYy' arg; do
	case $arg in
	(l)
		remote_user="$OPTARG"
		;;
	(o)
		case ${(L)OPTARG} in
		(hostname=*)
			hostname_at_argc=$((OPTIND-1))
			remote_hostname="${OPTARG#*=}"
			hostname_argv_prefix="${OPTARG%%=*}="
			;;
		(user=*)
			remote_user="${OPTARG#*=}"
			;;
		esac
		;;
	(p)
		remote_port="$OPTARG"
		;;
	(v)
		verbose=true
		;;
	esac
done

if [[ -n "${SSH_PDP_TRACE:-}" ]] || $verbose; then
	msg() { warn "$@"; }
else
	msg() { true; }
fi

# We might need to mutate the hostname, whether because of our punycode
# normalization or because of the mapping file switching things.  So we
# record any user@ prefix needed, together with the index at which to mutate.
if [[ -z "$remote_hostname" && $# -ge $OPTIND ]]; then
	hostname_at_argc="$OPTIND"
	d="${argv[$OPTIND]}"
	OPTIND=$((OPTIND+1))
	case "$d" in
	ssh://*)
		if [[ "$d" =~ '^ssh://([^@]+@)?([^:]+)(:[0-9]+)?$' ]]; then
			remote_hostname="${match[2]}"
			[[ -n "$remote_user" ]] || remote_user="${match[1]%@}"
			[[ -n "$remote_port" ]] || remote_port="${match[3]#:}"
			hostname_argv_prefix="ssh://${match[1]}"
			hostname_argv_suffix="${match[3]}"
		else
			warn "unable to parse SSH URL to extract information"
		fi
		;;
	*@*)
		remote_hostname="${d#*@}"
		hostname_argv_prefix="${d%%@*}@"
		if [[ -z "$remote_user" ]]; then
			remote_user="${d%%@*}"
		fi
		;;
	*)
		remote_hostname="$d"
		;;
	esac
	msg "remote_hostname=${(q-)remote_hostname} remote_user=${(q-)remote_user} remote_port=${(q-)remote_port}"
	unset d
fi
supplied_remote_hostname="$remote_hostname"

if (( OPTIND > $# )); then
	# no arguments left, we're probably interactive
	have_remote_cmd=false
	first_remote_argc='-1'
else
	have_remote_cmd=true
	first_remote_argc="$OPTIND"
fi

if [[ "$remote_hostname" == *[^[:ascii:]]* ]]; then
	if (( ${+commands[character]} )); then
		remote_hostname="$(character x-puny -o "$remote_hostname")"
	else
		warn "non-ASCII hostname ${(q)remote_hostname} seen, need a punycode converter"
	fi
fi

# Does case switching to set remote_env and possibly change ssh_invoke.
# SHOULD NOT change ssh_invoke, and if it does and prepends, then
# invoke_ssh_command_index had better be incremented too or chaos will result.
# *STRONGLY* prefer to add new intent vars to communicate intended behavioral
# change instead of directly manipulating.
#
# Might change remote_hostname.  Can set/override SSH_PDP_ROLE.
# Likely augments tab_coloring/color_schemes too.
# Have $color_variant available to help with Light/Dark
if [[ -f "$HOME/etc/site/ssh.hosts-mapping" ]]; then
	source "$HOME/etc/site/ssh.hosts-mapping"
fi

if [[ -n "${ssh_want_keepalive:-}" ]]; then
	if [[ "$ssh_want_keepalive" =~ '^[0-9]+$' ]]; then  # beware, not PCRE; POSIX ERE
		ssh_invoke+=(-o "ServerAliveInterval=$ssh_want_keepalive")
	else
		ssh_invoke+=(-o ServerAliveInterval=120)
	fi
fi

if [[ -n "${force_ssh_command:-}" ]]; then
	ssh_invoke[$invoke_ssh_command_index]="$force_ssh_command"
fi

# We keep this after sourcing ssh.hosts-mapping so that SSH_PDP_ROLE can be set.
if set_agent_socket; then
	# false means no keyfile; I think best to continue on, for ad-hoc roles
	load_per_agent_keys || true
else
	warn "continuing with unmodified ssh-agent"
fi

reset_all() {
	terminal_profile ITERM_PROFILE
	[[ -z ${tab_coloring[$remote_env]:-} ]] || tab_color reset
	[[ -z ${color_schemes[$remote_env]:-} ]] || terminal_color_scheme "$reset_color_scheme"
	set_tab_title "$HOST"
}
reset_title() { set_tab_title "$HOST"; }

if [[ "$remote_hostname" != "$supplied_remote_hostname" ]]; then
	if (( $hostname_at_argc < 0 )); then
		warn "need to massage hostname, don't know where, skipping"
	else
		repl="${hostname_argv_prefix}${remote_hostname}${hostname_argv_suffix}"
		argv[hostname_at_argc]=("$repl")
	fi
fi

if [[ -n "${SSH_PDP_TRACE:-}" ]] || $verbose; then
	warn "hostname=${(q-)remote_hostname} original=${(q-)supplied_remote_hostname} env=${(q-)remote_env} role=${(q-)SSH_PDP_ROLE:-} title=${(q-)remote_title} SSH_AUTH_SOCK=${(q-)SSH_AUTH_SOCK:-}"
	warn "${(@q+)ssh_invoke[*]} ${(@q+)*}"
	if [[ "$remote_hostname" == "does.not.exist" ]]; then warn "not invoking real ssh"; exit 0; fi
fi

just_exec=false

if [[ -n "${SSH_PDP_SKIP_TITLE:-}" ]]; then
	just_exec=true
elif $have_remote_cmd && ! $ssh_set_title_even_if_remote_cmd; then
	just_exec=true
elif [[ -n "${remote_env:-}" ]]; then
	set +e
	terminal_profile "$remote_env"
	[[ -z ${tab_coloring[$remote_env]:-} ]] || tab_color ${(s,:,)tab_coloring[$remote_env]}
	[[ -z ${color_schemes[$remote_env]:-} ]] || terminal_color_scheme "${color_schemes[$remote_env]}"
	if [[ -n "${remote_title:-}" ]]; then
		set_tab_title "${remote_title}"
	else
		set_tab_title "${remote_env}: ${remote_hostname}"
	fi
	resetter=reset_all
elif $remote_set_title; then
	case "$TERM" in
	xterm*)
		set_tab_title "${remote_title:-$remote_hostname}"
		resetter=reset_title
		;;
	*)
		just_exec=true
		;;
	esac
else
	just_exec=true
fi

if $just_exec; then
	exec "${ssh_invoke[@]}" "$@"
fi

trap "$resetter" INT
"${ssh_invoke[@]}" "$@"
ev=$?
"$resetter"
exit $ev

# vim: set ft=zsh :
-------------- next part --------------
#!/usr/bin/env -S zsh -feu
set -eu

# Our override env vars:
#  * SSH_PDP_COMMAND: ssh command or pathname to use (NOT scp)
#  * SSH_PDP_SCP_COMMAND: scp command or pathname to use
#  * SSH_PDP_BYPASS: if set and non-empty, then ignore all other logic except SSH_PDP_SCP_COMMAND
#  * SSH_PDP_ROLE: per-role SSH agent support; identifier
#  * SSH_PDP_ROLEFILE: location of non-default rolefile of keys
#  * SSH_PDP_TRACE: used for some extra debug info
# Our extra config files:
#  * ~/.ssh/role.<agentname> (or $SSH_PDP_ROLEFILE): ssh-keys to load
#  * ~/.ssh/agent-keyuse-confirm: existence prompts -c for ssh-add, same as ssh-add-today
#  * ~/etc/site/ssh.hosts-mapping: sourced, can change terminal profile names etc; also set the Role

progname="${0:A:t}"
progsuffix=".wrap"
warn() { printf >&2 '%s%s: %s\n' "$progname" "$progsuffix" "$*"; }
die() { warn "$@"; exit 1; }
have_cmd() { (( $+commands[$1] )) }

readonly DEFAULT_KEY_DURATION='20h'

# Finding the actual comand to call.
#
### case "${SSH_PDP_COMMAND:-ssh}" in
### 	/*) command="${SSH_PDP_COMMAND:-ssh}" ;;
case "${SSH_PDP_SCP_COMMAND:-scp}" in
	/*)
		command="${SSH_PDP_SCP_COMMAND:-scp}"
		if [[ -z "${SSH_PDP_COMMAND:-}" ]]; then
			next_ssh_command="${command%/scp}/ssh"
		else
			next_ssh_command="${SSH_PDP_COMMAND:?}"
			if [[ "$next_ssh_command" != /* ]]; then
				die "if SSH_PDP_SCP_COMMAND starts with / then SSH_PDP_COMMAND, if defined, must too"
				# laziness, don't want to rework the flow for the next section to handle resolution past us
			fi
		fi
		;;
	*)
		saved_PATH="$PATH"
		typeset -gU path
		#  We bias the result, for platforms where we need to keep system bins in path
		# first but we want a newer SSH
		path=( /opt/openssh/bin /opt/local/bin /usr/local/opt/openssh/bin /usr/local/bin "${path[@]}" )
		# We drop our own directory from path; -U makes it unique, only need to drop once
		path[(r)${0:h}]=()
		#
		command="$commands[${SSH_PDP_SCP_COMMAND:-scp}]"
		next_ssh_command="$commands[${SSH_PDP_COMMAND:-ssh}]"
		# We might use other utilities of our own below
		PATH="$saved_PATH"
		;;
esac

if [[ -n "${SSH_PDP_BYPASS:-}" ]]; then
	exec "$command" "$@"
fi

# Per-role dedicated SSH Agent support, so can have keys per role
# We define the functions here, but we only call them after sourcing the user
# mapping file, so that roles can be set per-hostname.
#
set_agent_socket_systemd() {
	if [[ "$SSH_PDP_ROLE" == "default" ]]; then
		want_socket="$XDG_RUNTIME_DIR/pdp.ssh/agent.systemd"
		export SSH_AUTH_SOCK="$want_socket"
		return
	fi
	want_socket="$XDG_RUNTIME_DIR/pdp.ssh/agent-${SSH_PDP_ROLE}.systemd"
	if [[ ! -S "$want_socket" ]]; then
		warn "Asking systemd to start ${(q-)SSH_PDP_ROLE} socket"
		systemctl --user start "ssh-agent-pdp@$SSH_PDP_ROLE"
		[[ -S "$want_socket" ]] || die "missing ${(q-)want_socket} after systemd user start"
	fi
	export SSH_AUTH_SOCK="$want_socket"
}
set_agent_socket() {
	[[ -n "${SSH_PDP_ROLE:-}" ]] || return 0
	case "$SSH_PDP_ROLE" in
	(*/* | *$'\0'* ) die "malformed SSH_PDP_ROLE" ;;
	esac

	if [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then
		if have_cmd systemctl; then
			set_agent_socket_systemd
			return $?
		fi
		warn "XDG-using platform, no systemctl"
	else
		warn "non-XDG platform"
	fi

	warn "unable to setup per-role ssh-agent for ${(q-)SSH_PDP_ROLE} on this platform"
	return 1
}
check_have_per_agent_keys() {
	ssh-add -l >/dev/null 2>&1
}

typeset -a scp_invoke
scp_invoke=( "$command" )

if [[ $OSTYPE == darwin* ]]
then
	if (( ${${OSTYPE#darwin}%.*} >= 12.0 ))
	then
		scp_invoke=(caffeinate -s "${scp_invoke[@]}")
	fi
fi

# Preserve API for loaded config file, even though not used for scp, only ssh
terminal_profile() { : ; }
tab_color() { : ; }
terminal_color_scheme() { : ; }
set_tab_title() { : ; }

# This is the variable set by lookup logic to say we're in an Interesting
# environment and the display should be mutated.
remote_env=''
# and this one for just "set the title but no other mutation"
remote_set_title=false
# this/these add to scp_invoke
ssh_want_keepalive=''  # number for interval, or just 'true'
# used to not set title when "ssh host <something>" by default, but perhaps we
# want that anyway, in which case override this.
ssh_set_title_even_if_remote_cmd=false
# For setting -O on a host-by-host basis
scp_want_old_rcp=false

# We don't consume getopts, so don't shift by OPTIND, we just want to find the remote hostname
# We also find the remote_user, so ssh mapping can augment decisions about which role might be needed
# Last confirmed up-to-date: OpenSSH 8.6p1
remote_user=''
remote_hostname=''
remote_port=''
ssh_at_argc=-1
ssh_command=''
hostname_at_argc=-1
hostname_argv_prefix=''
hostname_argv_suffix=''
port_at_argc=-1
verbose=false
cmdline_requested_old_scp=false
server_mode=false
# NB: scp invokes on the _remote_ side with options not in manual-page;
# we might be on the server side too, so need to support them.
# These server options are: -d -f -t (targetshouldbedirectory, from, to)
# Also existing but undocumented (so pass through): -1, -2 (protocol version)
#
# There's also M: but that appears to be a cruft bug:
#     added: 197e29f1cca190d767c4b2b63a662f9a9e5da0b3 2021-08-02
#   removed: 391ca67fb978252c48d20c910553f803f988bd37 2021-08-10
# (was replaced with -O/-s); do not support that.
# Updated: 2022-04-17 for OpenSSH 9.0 (and scp/sftp changes)
# nb: this is copy/paste from scp source, for completeness, not ordered by me
if [[ "$#" -eq 1 && "$1" == "--version" ]]; then
	printf >&2 'scp.wrap: '
	"$next_ssh_command" -V
	exit 0
fi
while getopts '12346ABCTdfOpqRrstvD:F:J:M:P:S:c:i:l:o:' arg; do
	case $arg in
	(S)
		ssh_at_argc=$((OPTIND-1))
		ssh_command="$OPTARG"
		;;
	(o)
		case ${(L)OPTARG} in
		(hostname=*)
			hostname_at_argc=$((OPTIND-1))
			remote_hostname="${OPTARG#*=}"
			hostname_argv_prefix="${OPTARG%%=*}="
			;;
		(user=*)
			remote_user="${OPTARG#*=}"
			;;
		esac
		;;
	(O)
		cmdline_requested_old_scp=true
		;;
	(P)
		remote_port="$OPTARG"
		port_at_argc=$((OPTIND-1))
		;;
	(v)
		verbose=true
		# scp takes -v too, this is pass-through.
		;;
	(f)
		progsuffix+="/svr-from"
		server_mode=true
		;;
	(t)
		progsuffix+="/svr-to"
		server_mode=true
		;;
	esac
done

if [[ -n "${SSH_PDP_TRACE:-}" ]] || $verbose; then
	msg() { warn "$@"; }
else
	msg() { true; }
fi

# We might need to mutate the hostname, whether because of our punycode
# normalization or because of the mapping file switching things.  So we
# record any user@ prefix needed, together with the index at which to mutate.
# For two-remotes invocations, the mutated hostname will always be the first
# (we're not really designed to handle that, this logic was written for ssh
# not scp).
if [[ -z "$remote_hostname" && $# -ge $((OPTIND + 1)) ]]; then
	first_host_index="$OPTIND"
	if [[ ! "${argv[$first_host_index]}" == *:* ]]; then
		# Not "the second after the options", but "the last"
		# eg: scp local1 local2 local3 remote:/target/dir/
		first_host_index=$#
		if [[ ! "${argv[$first_host_index]}" == *:* ]]; then
			warn "no colon found at argv $OPTIND or $#, scp will copy locally?"
			first_host_index=''
		fi
	fi
fi
if [[ -n "${first_host_index:-}" ]]; then
	hostname_at_argc="$first_host_index"
	d="${argv[$first_host_index]}"
	case "$d" in
	scp://*)
		if [[ "$d" =~ '^scp://([^@]+@)?([^:/]+)(:[0-9]+)?(/.*)?$' ]]; then
			remote_hostname="${match[2]}"
			[[ -n "$remote_user" ]] || remote_user="${match[1]%@}"
			[[ -n "$remote_port" ]] || remote_port="${match[3]#:}"
			hostname_argv_prefix="ssh://${match[1]}"
			hostname_argv_suffix="${match[3]}${match[4]}"
		else
			warn "unable to parse SCP URL to extract information"
		fi
		;;
	*@*:*)
		remote_hostname="${d#*@}"
		hostname_argv_prefix="${d%%@*}@"
		hostname_argv_suffix=":${remote_hostname#*:}"
		remote_hostname="${remote_hostname%%:*}"
		if [[ -z "$remote_user" ]]; then
			remote_user="${d%%@*}"
		fi
		;;
	*:*)
		remote_hostname="${d%%:*}"
		hostname_argv_suffix=":${d#*:}"
		;;
	*)
		msg "unhandled remote hostname candidate: ${(q-)d}"
		;;
	esac
	msg "remote_hostname=${(q-)remote_hostname} remote_user=${(q-)remote_user} remote_port=${(q-)remote_port}"
	unset d
fi
supplied_remote_hostname="$remote_hostname"
supplied_remote_port="$remote_port"

if [[ "$remote_hostname" == *[^[:ascii:]]* ]]; then
	if (( ${+commands[character]} )); then
		remote_hostname="$(character x-puny -o "$remote_hostname")"
	else
		warn "non-ASCII hostname ${(q)remote_hostname} seen, need a punycode converter"
	fi
fi

supplied_ssh_command="$ssh_command"
force_ssh_command=''

# Does case switching to set remote_env and possibly change scp_invoke.
# Might change remote_hostname.  Can set/override SSH_PDP_ROLE.
# Can change ssh_command to force use of a historical-cryptosuite-supporting variant.
# force_ssh_command can be set, to override our default of bypassing our
# wrappers for the scp invocation of ssh.
if $server_mode; then
	: # we do not source a config file as a server
elif [[ -f "$HOME/etc/site/ssh.hosts-mapping" ]]; then
	source "$HOME/etc/site/ssh.hosts-mapping"
fi

if [[ -n "${ssh_want_keepalive:-}" ]]; then
	if [[ "$ssh_want_keepalive" =~ '^[0-9]+$' ]]; then  # beware, not PCRE; POSIX ERE
		scp_invoke+=(-o "ServerAliveInterval=$ssh_want_keepalive")
	else
		scp_invoke+=(-o ServerAliveInterval=120)
	fi
fi

# We keep this after sourcing ssh.hosts-mapping so that SSH_PDP_ROLE can be set.
if $server_mode; then
	: # no keys to load as an scp server
elif set_agent_socket; then
	check_have_per_agent_keys || warn "no keys loaded for target SSH role"
else
	warn "continuing with unmodified ssh-agent"
fi

if [[ "$remote_hostname" != "$supplied_remote_hostname" ]]; then
	if (( $hostname_at_argc < 0 )); then
		warn "need to massage hostname, don't know where, skipping"
	else
		repl="${hostname_argv_prefix}${remote_hostname}${hostname_argv_suffix}"
		argv[hostname_at_argc]=("$repl")
	fi
fi

# This doesn't handle port being derived from anything other than -P.
if [[ "$remote_port" != "$supplied_remote_port" ]]; then
	msg "port remapped ${(q-)supplied_remote_port} -> ${(q-)remote_port}"
	if (( $port_at_argc < 0 )); then
		scp_invoke+=( -P "$remote_port" )
	else
		argc[port_at_argc]=("$remote_port")
	fi
fi

if $scp_want_old_rcp && ! $cmdline_requested_old_scp; then
	scp_invoke+=(-O)
fi

if [[ -n "$force_ssh_command" ]]; then
	ssh_command="$force_ssh_command"
elif [[ "$ssh_command" == "$supplied_ssh_command" ]]; then
	# This should be the normal case.
	# The scp layer will, in this self script, have overridden the
	# environment variables as appropriate to talk to the correct socket.
	# There's no need to redo all the logic again in the ssh layer.
	#
	# Except, scp by default will look at argv[0] to figure out the path
	# for ssh, so we don't need to do this _unless_ there's an explicit
	# instance in argv and it's unqualified.
	# But if we explicitly set 'ssh.pdp' then ... don't?
	#
	# Aargh, this is a chaotic mess, I'd rip out the ssh-command tracking
	# as a bad idea except that I just know that I will need the ability to
	# override for some ancient crap client only speaking an insecure
	# ciphersuite, sooner or later.
	# So ... only do it if it's "ssh".
	#
	# After all, if it were only for optimization, we could get 90% of the
	# way there by exporting SSH_PDP_BYPASS=from-scp into environ, so that
	# ssh.pdp would early-exec.
	if (( $ssh_at_argc > 0 )) && [[ "$ssh_command" == ssh ]]; then
		ssh_command="$next_ssh_command"
	fi
fi

if [[ "$ssh_command" != "$supplied_ssh_command" ]]; then
	if (( $ssh_at_argc < 0 )); then
		scp_invoke+=(-S "$ssh_command")
	else
		argv[ssh_at_argc]="$ssh_command"
	fi
fi

if [[ -n "${SSH_PDP_TRACE:-}" ]] || $verbose; then
	warn "hostname=${(q-)remote_hostname} original=${(q-)supplied_remote_hostname} env=${(q-)remote_env} role=${(q-)SSH_PDP_ROLE:-} SSH_AUTH_SOCK=${(q-)SSH_AUTH_SOCK:-}"
	warn "${(@q+)scp_invoke[*]} ${(@q+)*}"
	if [[ "$remote_hostname" == "does.not.exist" ]]; then warn "not invoking real scp"; exit 0; fi
fi

# If we do call back into ssh.pdp or ourselves at some point, then as long as
# SSH_PDP_BYPASS is not passed to a remote system (it shouldn't be), this will
# short-circuit the already-done work.
export SSH_PDP_BYPASS='from-scp'

exec "${scp_invoke[@]}" "$@"

# vim: set ft=zsh :
-------------- next part --------------
if ! [ -n "${remote_hostname:-}" ]; then
  # ssh without a remote probably means "ssh -V" or other query.
  # we should avoid interfering; there's nothing else for us to do here though.
  return 0
fi

prior_role="${SSH_PDP_ROLE:-}"
case "${remote_hostname%.lan}" in
  forge-*)       SSH_PDP_ROLE=foo-prod ;;
  bastion1)      SSH_PDP_ROLE=foo-prod ;;
  # For now, assume that anything AWS is for Foo
  *.amazonaws.com)  SSH_PDP_ROLE=foo-prod ;;

  oldhost)  scp_want_old_rcp=true ;;
  debug-local)  remote_hostname='localhost'; remote_port=26; scp_want_old_rcp=true ;;
esac

if [ "$remote_port" = '2222' ] && [ "$remote_user" = 'special' ]; then
  msg "foo matched"
  SSH_PDP_ROLE=foo-prod
fi

if [ -n "${GIT_PROTOCOL:-}" ] && [ -n "${GIT_EXEC_PATH:-}" ]; then
  # We are being called as ssh inside git
  remote_set_title=false
  remote_env=''
fi

# Other stuff I might check here in a non-trimmed version:
#  [ -n "${PACKER_RUN_UUID:-}" ]
#  [ "${GIT_REFLOG_ACTION:-}" = "" ]

# sourced by zsh but try to keep it sh-friendly
# vim: set ft=sh sw=2 et :


More information about the openssh-unix-dev mailing list