package ssh import ( "fmt" "strings" ) // Client describes one OpenSSH program netssh can run. The programs share // ssh's connection options but differ in flags and destination syntax. type Client struct { Name string // binary name: ssh, sftp, or scp withArg map[byte]bool // short flags that consume a value port byte // flag for the port: -p (ssh) or -P (sftp, scp) user byte // flag for the login user; 0 when only user@ or -o User work paths bool // destinations are host[:path] } var ( // SSH is the default client; its flag table is flagsWithArg. SSH = Client{Name: "ssh", withArg: flagsWithArg, port: 'p', user: 'l'} // SFTP follows sftp(1): -l is a bandwidth limit, -P the port. SFTP = Client{Name: "sftp", withArg: flagSet("BbcDFiJloPRSsX"), port: 'P', paths: true} // SCP follows scp(1); unlike sftp, -B and -R take no value. SCP = Client{Name: "scp", withArg: flagSet("cDFiJloPSX"), port: 'P', paths: true} ) func flagSet(flags string) map[byte]bool { set := make(map[byte]bool, len(flags)) for i := 0; i < len(flags); i++ { set[flags[i]] = true } return set } // PortArgs returns the arguments that set port for this client. func (c Client) PortArgs(port string) []string { return []string{"-" + string(c.port), port} } // UserArgs returns the arguments that set the login user for this client. func (c Client) UserArgs(user string) []string { if c.user == 0 { return []string{"-o", "User=" + user} } return []string{"-" + string(c.user), user} } // Operand is a remote scp argument such as user@host:/path. type Operand struct { Idx int User string Host string Path string } // Operands returns the remote operands of scp arguments. All remote operands // must name the same host, because one set of options applies per call. func (c Client) Operands(args []string) ([]Operand, error) { start := c.optionsEnd(args) var out []Operand for i := start; i < len(args); i++ { arg := args[i] if strings.HasPrefix(arg, "scp://") || strings.HasPrefix(arg, "sftp://") { return nil, fmt.Errorf("%q: use host:path instead of a URI", arg) } dest, path, remote := splitRemote(arg) if !remote { continue } host, user := splitUserHost(dest) if host == "" { continue } if len(out) > 0 && !strings.EqualFold(out[0].Host, host) { return nil, fmt.Errorf("scp between two remote hosts (%s, %s) is not supported; copy in two steps", out[0].Host, host) } out = append(out, Operand{Idx: i, User: user, Host: host, Path: path}) } return out, nil } // optionsEnd returns the index of the first operand; scp, like ssh, stops // option parsing at the first non-option or after "--". func (c Client) optionsEnd(args []string) int { end := c.insertionPoint(args) if end < len(args) && args[end] == "--" { end++ } return end } // insertionPoint is where added options still count as options: before the // first operand or before "--". func (c Client) insertionPoint(args []string) int { for i := 0; i < len(args); i++ { arg := args[i] if arg == "--" { return i } if !strings.HasPrefix(arg, "-") || len(arg) < 2 { return i } if c.takesSeparateValue(arg) { i++ } } return len(args) } // splitRemote mirrors scp's colon(): an argument is remote when a ':' comes // before any '/'. A leading "[" starts a bracketed IPv6 address. func splitRemote(arg string) (dest, path string, remote bool) { if strings.HasPrefix(arg, "/") || strings.HasPrefix(arg, ".") { return "", "", false } inBracket := false for i := 0; i < len(arg); i++ { switch arg[i] { case '[': inBracket = true case ']': inBracket = false case '/': if !inBracket { return "", "", false } case ':': if !inBracket { return arg[:i], arg[i+1:], true } } } return "", "", false } // HasForwardFlag reports whether -L, -R, or -D (or the -o equivalents) was given. func (c Client) HasForwardFlag(args []string) bool { return c.hasOption(args, 'L', "LocalForward") || c.hasOption(args, 'R', "RemoteForward") || c.hasOption(args, 'D', "DynamicForward") }