Files
Sebastian UnterschützandClaude Opus 5.5 55e7a5106b fix: interface_name strategy, instance hosts in sftp/scp, alias rewrites
Found by the new Docker test environment against a real NetBox 4.7:

- The interface_name strategy filtered IP addresses with interface_name
  and vminterface_name, which NetBox ignores, so it returned the first
  address of the host instead of the named interface's. Use the real
  filters interface and vminterface; a missing interface (HTTP 400)
  falls through to the next strategy.
- sftp and scp took instance-qualified hosts (prod/web01:/path) for
  local paths, because a "/" comes before the ":". Such arguments are
  remote when the prefix is a configured NetBox instance.
- Shell aliases of instance-qualified hosts ran the shortcut rules on
  "instance/host", so anchored rewrite rules never matched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 12:30:24 +01:00

156 lines
4.2 KiB
Go

package ssh
import "strings"
// flagsWithArg lists all SSH flags that consume the following argument.
var flagsWithArg = map[byte]bool{
'B': true, 'P': true, 'b': true, 'c': true, 'D': true, 'E': true, 'e': true,
'F': true, 'I': true, 'i': true, 'J': true, 'L': true,
'l': true, 'm': true, 'o': true, 'O': true, 'p': true,
'Q': true, 'R': true, 'S': true, 'w': true, 'W': true,
}
// ParsedArgs holds the result of parsing SSH arguments.
type ParsedArgs struct {
Host string // hostname without the user@ prefix or :path
User string // empty if not specified
Path string // sftp only: the remote path after host:
DestIdx int // index in Args where [user@]host sits
Args []string
}
// Parse scans SSH arguments and extracts the destination ([user@]host).
// Returns nil if no destination is found.
func Parse(args []string) *ParsedArgs { return SSH.Parse(args) }
// Parse extracts the destination: [user@]host, or [user@]host[:path] for sftp.
func (c Client) Parse(args []string) *ParsedArgs {
i := 0
for i < len(args) {
arg := args[i]
// "--" ends option processing
if arg == "--" {
i++
break
}
if strings.HasPrefix(arg, "-") && len(arg) > 1 {
// OpenSSH accepts clusters such as -vp 2222 and -vladmin.
if c.takesSeparateValue(arg) {
i++
}
i++
continue
}
// first non-flag argument is the destination
return c.parsed(args, i)
}
// handle arguments after "--"
if i < len(args) {
return c.parsed(args, i)
}
return nil
}
func (c Client) parsed(args []string, i int) *ParsedArgs {
dest, path := args[i], ""
if c.paths {
if d, p, remote := c.splitRemote(dest); remote {
dest, path = d, p
}
}
host, user := splitUserHost(dest)
return &ParsedArgs{Host: host, User: user, Path: path, DestIdx: i, Args: args}
}
// ReplaceHost returns a copy of args with the destination replaced by newHost,
// preserving any user@ prefix.
func ReplaceHost(args []string, destIdx int, newHost string) []string {
return SSH.replaceHost(args, []int{destIdx}, newHost)
}
// replaceHost keeps user@ and, for sftp and scp, :path. Those clients read
// host:path, so an IPv6 address must be bracketed.
func (c Client) replaceHost(args []string, dests []int, newHost string) []string {
result := make([]string, len(args))
copy(result, args)
if c.paths && strings.Contains(newHost, ":") {
newHost = "[" + newHost + "]"
}
for _, destIdx := range dests {
original := args[destIdx]
prefix, suffix := "", ""
if at := strings.Index(original, "@"); at != -1 {
prefix, original = original[:at+1], original[at+1:]
}
if c.paths {
if _, path, remote := c.splitRemote(original); remote {
suffix = ":" + path
}
}
result[destIdx] = prefix + newHost + suffix
}
return result
}
// HasPortFlag reports whether a port was explicitly supplied.
func HasPortFlag(args []string) bool { return SSH.HasPortFlag(args) }
// HasUserFlag reports whether a login user was explicitly supplied.
func HasUserFlag(args []string) bool { return SSH.HasUserFlag(args) }
// HasPortFlag reports whether a port was explicitly supplied.
func (c Client) HasPortFlag(args []string) bool { return c.hasOption(args, c.port, "Port") }
// HasUserFlag reports whether a login user was explicitly supplied.
func (c Client) HasUserFlag(args []string) bool { return c.hasOption(args, c.user, "User") }
func hasOption(args []string, wanted byte, keyword string) bool {
return SSH.hasOption(args, wanted, keyword)
}
func (c Client) hasOption(args []string, wanted byte, keyword string) bool {
for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" || len(arg) < 2 || arg[0] != '-' {
break
}
for j := 1; j < len(arg); j++ {
flag := arg[j]
if !c.withArg[flag] {
continue
}
value := arg[j+1:]
if value == "" {
i++
if i >= len(args) {
return false
}
value = args[i]
}
if wanted != 0 && flag == wanted {
return true
}
if flag == 'o' {
fields := strings.FieldsFunc(value, func(r rune) bool { return r == '=' || r == ' ' || r == '\t' })
if len(fields) >= 2 && strings.EqualFold(fields[0], keyword) {
return true
}
}
break
}
}
return false
}
func splitUserHost(dest string) (host, user string) {
if at := strings.Index(dest, "@"); at != -1 {
return dest[at+1:], dest[:at]
}
return dest, ""
}