package ssh import ( "reflect" "strings" "testing" "git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config" ) func TestSFTPParse(t *testing.T) { // -l is a bandwidth limit and -s takes a value for sftp, unlike ssh. parsed := SFTP.Parse([]string{"-l", "100", "-s", "sub", "-P", "2222", "ops@web01:/var/log"}) if parsed == nil || parsed.Host != "web01" || parsed.User != "ops" || parsed.Path != "/var/log" || parsed.DestIdx != 6 { t.Fatalf("got %+v", parsed) } if p := SFTP.Parse([]string{"prod/web01"}); p == nil || p.Host != "prod/web01" || p.Path != "" { t.Fatalf("instance-qualified host: %+v", p) } if !SFTP.HasPortFlag([]string{"-P", "2222", "web"}) || SFTP.HasPortFlag([]string{"-p", "web"}) { t.Fatal("sftp port flag is -P") } if SFTP.HasUserFlag([]string{"-l", "100", "web"}) || !SFTP.HasUserFlag([]string{"-o", "User=ops", "web"}) { t.Fatal("sftp has no -l user flag") } } func TestSCPOperands(t *testing.T) { ops, err := SCP.Operands([]string{"-X", "nrequests=8", "-r", "./local:dir", "/abs/file", "ops@web01:/tmp/", "web01:b"}) if err != nil { t.Fatal(err) } want := []Operand{{Idx: 5, User: "ops", Host: "web01", Path: "/tmp/"}, {Idx: 6, Host: "web01", Path: "b"}} if !reflect.DeepEqual(ops, want) { t.Fatalf("got %+v", ops) } if ops, _ := SCP.Operands([]string{"a.txt", "b.txt"}); len(ops) != 0 { t.Fatalf("local copy has remote operands: %+v", ops) } if _, err := SCP.Operands([]string{"web01:a", "db01:b"}); err == nil { t.Fatal("two remote hosts accepted") } if _, err := SCP.Operands([]string{"scp://web01/a", "."}); err == nil { t.Fatal("URI accepted") } } func TestSFTPApply(t *testing.T) { args := []string{"-P", "2222", "ops@web01:/var/log"} opts := config.SSHOptions{ProxyJump: "jh", ExtraArgs: []string{"-A"}, Forwards: map[string]config.Forward{"web": {Local: "1:a:2"}}} got := SFTP.Apply(args, []int{2}, Host{Name: "web01", IP: "192.0.2.10"}, opts, WrapperFlags{ForwardArgs: []string{"-L", "1:a:2"}}).Args want := []string{"-P", "2222", "-J", "jh", "ops@192.0.2.10:/var/log"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v (extra_args and forwards are ssh only)", got, want) } } func TestSCPApplyReplacesEveryOperandAndBracketsIPv6(t *testing.T) { args := []string{"a.txt", "web01:/tmp/", "ops@web01:b"} got := SCP.Apply(args, []int{1, 2}, Host{Name: "web01", IP: "2001:db8::1"}, config.SSHOptions{HostKeyChecking: "accept-new"}, WrapperFlags{}).Args want := []string{"-o", "StrictHostKeyChecking=accept-new", "a.txt", "[2001:db8::1]:/tmp/", "ops@[2001:db8::1]:b"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v", got) } // sftp needs brackets even without a path; ssh never does. if got := SFTP.Apply([]string{"web01"}, []int{0}, Host{Name: "web01", IP: "2001:db8::1"}, config.SSHOptions{}, WrapperFlags{}).Args; got[0] != "[2001:db8::1]" { t.Fatalf("sftp: %v", got) } if got := apply([]string{"web01"}, config.SSHOptions{}, WrapperFlags{}); got.Args[0] != "192.0.2.10" { t.Fatalf("ssh: %v", got.Args) } } func TestSFTPTargetName(t *testing.T) { got := SFTP.Apply([]string{"web01:/srv"}, []int{0}, Host{Name: "web01.example.com", IP: "192.0.2.10"}, config.SSHOptions{Target: "name"}, WrapperFlags{}).Args want := []string{"-o", "HostName=192.0.2.10", "-o", "HostKeyAlias=web01.example.com", "web01.example.com:/srv"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v", got) } } func TestClientUserAndPortArgs(t *testing.T) { if got := SSH.UserArgs("ops"); !reflect.DeepEqual(got, []string{"-l", "ops"}) { t.Fatal(got) } if got := SCP.UserArgs("ops"); !reflect.DeepEqual(got, []string{"-o", "User=ops"}) { t.Fatal(got) } if got := SFTP.PortArgs("2222"); !reflect.DeepEqual(got, []string{"-P", "2222"}) { t.Fatal(got) } } func TestClientExtractWrapperFlags(t *testing.T) { // For scp, -X takes a value; for ssh it does not. flags, rest, err := SCP.ExtractWrapperFlags([]string{"-X", "--dry-run", "--dry-run", "a", "web:b"}) if err != nil || !flags.DryRun || !reflect.DeepEqual(rest, []string{"-X", "--dry-run", "a", "web:b"}) { t.Fatalf("got %+v %v %v", flags, rest, err) } flags, rest, err = ExtractWrapperFlags([]string{"--forward", "web", "--forward=8080:localhost:80", "-v", "web01"}) if err != nil || !reflect.DeepEqual(flags.Forwards, []string{"web", "8080:localhost:80"}) || !reflect.DeepEqual(rest, []string{"-v", "web01"}) { t.Fatalf("got %+v %v %v", flags, rest, err) } for _, args := range [][]string{{"--forward"}, {"--forward", "-L", "web"}} { if _, _, err := ExtractWrapperFlags(args); err == nil { t.Errorf("%v accepted", args) } } } func TestForwardArgs(t *testing.T) { available := map[string]config.Forward{"web": {Local: "8080:localhost:80"}, "socks": {Dynamic: "1080"}, "back": {Remote: "9000:localhost:9000"}} got, err := ForwardArgs([]string{"WEB", "socks", "back", "5432:db:5432", "127.0.0.1:3000:localhost:3000"}, available) want := []string{"-L", "8080:localhost:80", "-D", "1080", "-R", "9000:localhost:9000", "-L", "5432:db:5432", "-L", "127.0.0.1:3000:localhost:3000"} if err != nil || !reflect.DeepEqual(got, want) { t.Fatalf("got %v %v", got, err) } if _, err := ForwardArgs([]string{"db"}, available); err == nil || !strings.Contains(err.Error(), "back, socks, web") { t.Fatalf("unknown name: %v", err) } if _, err := ForwardArgs([]string{"db"}, nil); err == nil || !strings.Contains(err.Error(), "no forwards are configured") { t.Fatalf("no forwards: %v", err) } } func TestApplyForwardsAndTunnel(t *testing.T) { flags := WrapperFlags{ForwardArgs: []string{"-L", "8080:localhost:80"}} if got := apply([]string{"web"}, config.SSHOptions{}, flags).Args; !reflect.DeepEqual(got, []string{"-L", "8080:localhost:80", "192.0.2.10"}) { t.Fatalf("forward: %v", got) } flags.Tunnel = true result := apply([]string{"web"}, config.SSHOptions{}, flags) want := []string{"-L", "8080:localhost:80", "-o", "ExitOnForwardFailure=yes", "-N", "192.0.2.10"} if !reflect.DeepEqual(result.Args, want) || !strings.Contains(result.Notice, "forwarding -L 8080:localhost:80 via web.example.com") { t.Fatalf("tunnel: %v %q", result.Args, result.Notice) } // The user's own ExitOnForwardFailure wins. if got := apply([]string{"-o", "ExitOnForwardFailure=no", "web"}, config.SSHOptions{}, flags).Args; strings.Contains(strings.Join(got, " "), "=yes") { t.Fatalf("overrode user option: %v", got) } } func TestHasForwardFlag(t *testing.T) { for args, want := range map[string]bool{"-L 1:a:2 web": true, "-vD1080 web": true, "-o RemoteForward=1_a_2 web": true, "-l ops web": false, "web -L 1:a:2": false} { if got := SSH.HasForwardFlag(strings.Fields(args)); got != want { t.Errorf("%q: got %v", args, got) } } } func TestApplyInsertsOptionsBeforeDoubleDash(t *testing.T) { got := Apply([]string{"-v", "--", "web"}, 2, Host{Name: "web", IP: "192.0.2.10"}, config.SSHOptions{ProxyJump: "jh"}, WrapperFlags{}).Args if want := []string{"-v", "-J", "jh", "--", "192.0.2.10"}; !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) } } // Instance-qualified hosts contain "/" before the ":" of host:path; only // configured instances make such an argument remote. func TestInstanceQualifiedHostPath(t *testing.T) { scp := SCP.WithInstances([]string{"prod"}) ops, err := scp.Operands([]string{"a.txt", "ops@prod/web01:/tmp/", "dir/file:x"}) if err != nil { t.Fatal(err) } if want := []Operand{{Idx: 1, User: "ops", Host: "prod/web01", Path: "/tmp/"}}; !reflect.DeepEqual(ops, want) { t.Fatalf("got %+v", ops) } got := scp.Apply([]string{"a.txt", "ops@prod/web01:/tmp/"}, []int{1}, Host{Name: "web01", IP: "192.0.2.1"}, config.SSHOptions{}, WrapperFlags{}).Args if want := []string{"a.txt", "ops@192.0.2.1:/tmp/"}; !reflect.DeepEqual(got, want) { t.Fatalf("apply: %v", got) } sftp := SFTP.WithInstances([]string{"prod"}) if p := sftp.Parse([]string{"prod/web01:/var/log"}); p == nil || p.Host != "prod/web01" || p.Path != "/var/log" { t.Fatalf("sftp: %+v", p) } if ops, _ := SCP.Operands([]string{"prod/web01:/tmp/", "."}); len(ops) != 0 { t.Fatalf("unknown instance must stay local: %+v", ops) } }