class Net::SSH::Multiplex::ControlPath
%u local user
%r remote user
%p remote port
%n the host name as given
%l local host name
%L local host name up to the first dot
%k the host key alias, or the host name as given
%j host of the last ProxyJump hop, or empty
%i local uid
%h remote host name, after HostName
%d local home directory
%C SHA1 hex of %l%h%p%r%j
%% a literal %
percent_dollar_expand).
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS in sshconnect.h, misc.c
then ${ENV} and the % tokens below in a single pass (ssh.c,
Expands a ControlPath the way OpenSSH’s ssh does: a leading ~ first,
def self.jump_host(proxy_jump)
The host ssh uses for %j: the last hop of a ProxyJump list, without
def self.jump_host(proxy_jump) last = proxy_jump.to_s.split(',').last.to_s.strip.delete_prefix('ssh://') last = last.split('@').last.to_s if last.start_with?('[') last[1...last.index(']').to_i] else last.split(':').first.to_s end end
def address?(host)
def address?(host) IPAddr.new(host) true rescue IPAddr::Error false end
def expand(path)
variable or an unknown ~user, which make ssh refuse the ControlPath
Raises ArgumentError for an unknown % token, an unset ${ENV}
def expand(path) expand_tokens(expand_tilde(path)) end
def expand_tilde(path)
ssh uses the home directory from the password database, not $HOME
def expand_tilde(path) return path unless path.start_with?('~') user, rest = path[1..].split('/', 2) home = user.nil? || user.empty? ? Etc.getpwuid(Process.uid).dir : Etc.getpwnam(user).dir rest.nil? ? home : File.join(home, rest) end
def expand_tokens(path)
def expand_tokens(path) path.gsub(/\$\{([^}]*)\}|%(.?)|\$\{.*/m) do if Regexp.last_match(1) name = Regexp.last_match(1) raise ArgumentError, "empty ${} variable" if name.empty? ENV.fetch(name) { raise ArgumentError, "variable ${#{name}} is not set" } elsif Regexp.last_match(2) token = Regexp.last_match(2) next '%' if token == '%' tokens.fetch(token) { raise ArgumentError, "unknown token %#{token}" } else raise ArgumentError, "unterminated ${" end end end
def initialize(host_arg:, host:, port:, remote_user:, jump_host: '', host_key_alias: nil) # rubocop:disable Metrics/ParameterLists
HostName.
+host_arg+ is the host as given, +host+ the one connected to after
def initialize(host_arg:, host:, port:, remote_user:, jump_host: '', host_key_alias: nil) # rubocop:disable Metrics/ParameterLists @host_arg = host_arg.to_s # ssh lowercases host names, but not addresses, before expanding. @host = address?(host.to_s) ? host.to_s : host.to_s.downcase @port = port.to_s @remote_user = remote_user.to_s @jump_host = jump_host.to_s @host_key_alias = host_key_alias end
def tokens
def tokens @tokens ||= begin pw = Etc.getpwuid(Process.uid) local_host = Socket.gethostname { 'C' => Digest::SHA1.hexdigest("#{local_host}#{@host}#{@port}#{@remote_user}#{@jump_host}"), 'd' => pw.dir, 'h' => @host, 'i' => Process.uid.to_s, 'j' => @jump_host, 'k' => @host_key_alias || @host_arg, 'L' => local_host.split('.', 2).first, 'l' => local_host, 'n' => @host_arg, 'p' => @port, 'r' => @remote_user, 'u' => pw.name } end end