add Realpath client public fn and fix server impl
diff --git a/client.go b/client.go index f0d67e8..4aee6dd 100644 --- a/client.go +++ b/client.go
@@ -611,6 +611,11 @@ } } +// Realpath requests the canonical name (absolute path, symlinks resolved) of a requested path +func (c *Client) Realpath(path string) (string, error) { + return c.realpath(path) +} + func (c *Client) realpath(path string) (string, error) { id := c.nextID() typ, data, err := c.sendRequest(sshFxpRealpathPacket{
diff --git a/client_integration_test.go b/client_integration_test.go index 8fa7f95..6f3f537 100644 --- a/client_integration_test.go +++ b/client_integration_test.go
@@ -578,6 +578,47 @@ } } +func TestClientRealpath(t *testing.T) { + sftp, cmd := testClient(t, READONLY, NO_DELAY) + defer cmd.Wait() + defer sftp.Close() + + f, err := ioutil.TempFile("", "sftptest") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + + realName, err := filepath.EvalSymlinks(f.Name()) + if err != nil { + t.Fatal(err) + } + realName, err = filepath.Abs(realName) + if err != nil { + t.Fatal(err) + } + realName = filepath.Clean(realName) + linkName := realName + ".softlink" + + // create a symlink that points at sftptest + if err := os.Symlink(realName, linkName); err != nil { + t.Fatal(err) + } + defer os.Remove(linkName) + + // compare names + want := realName + got, err := sftp.Realpath(linkName) + if err != nil { + t.Fatal(err) + } + + // check that realpath is valid + if want != got { + t.Fatalf("Realpath(%q): got %v", want, got) + } +} + func TestClientSymlink(t *testing.T) { sftp, cmd := testClient(t, READWRITE, NO_DELAY) defer cmd.Wait()
diff --git a/server.go b/server.go index 81b1659..1c6f0ae 100644 --- a/server.go +++ b/server.go
@@ -361,7 +361,12 @@ func (p sshFxpRealpathPacket) readonly() bool { return true } func (p sshFxpRealpathPacket) respond(svr *Server) error { - f, err := filepath.Abs(p.Path) + f, err := filepath.EvalSymlinks(p.Path) + if err != nil { + return svr.sendPacket(statusFromError(p.ID, err)) + } + + f, err = filepath.Abs(f) if err != nil { return svr.sendPacket(statusFromError(p.ID, err)) }
diff --git a/server_integration_test.go b/server_integration_test.go index 4d70829..7772ac1 100644 --- a/server_integration_test.go +++ b/server_integration_test.go
@@ -503,6 +503,63 @@ } } +func TestServerRealpath(t *testing.T) { + // create servers of each variant + listenerGo, hostGo, portGo := testServer(t, GOLANG_SFTP, READONLY) + listenerOp, hostOp, portOp := testServer(t, OPENSSH_SFTP, READONLY) + defer listenerGo.Close() + defer listenerOp.Close() + + // create clients for each server + clientConfig := ssh.ClientConfig{ + Config: ssh.Config{ }, + User: "test", + Auth: []ssh.AuthMethod{ssh.Password("")}, + } + clientConfigGo := clientConfig + clientConfigOp := clientConfig + sshClientGo, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", hostGo, portGo), &clientConfigGo) + if err != nil { + t.Fatal(err) + } + sshClientOp, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", hostOp, portOp), &clientConfigOp) + if err != nil { + t.Fatal(err) + } + sftpClientGo, err := NewClient(sshClientGo) + if err != nil { + t.Fatal(err) + } + sftpClientOp, err := NewClient(sshClientOp) + if err != nil { + t.Fatal(err) + } + + // create a file, and a convoluted path under it + dir, err := ioutil.TempDir("", "sftptestServerRealpath") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + f1 := path.Join(dir, "a", "b", "b.txt") + f1strange := strings.Join([]string{dir, "a", "b", "c", "d", "e", "..", "..", "..", "b.txt"}, string(os.PathSeparator)) + if err := os.MkdirAll(path.Join(dir, "a", "b", "c", "d", "e"), 0755); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(f1, []byte("hello"), 0644); err != nil { + t.Fatal(err) + } + + // compare output from go and openssh + if outputGo, err := sftpClientGo.Realpath(f1strange); err != nil { + t.Fatalf("failed: %v %v", err, string(outputGo)) + } else if outputOp, err := sftpClientOp.Realpath(f1strange); err != nil { + t.Fatalf("failed: %v %v", err, string(outputOp)) + } else if outputGo != outputOp { + t.Fatalf("output differs:\ngo: '%v'\nop: '%v'\n", outputGo, outputOp) + } +} + func TestServerPut(t *testing.T) { listenerGo, hostGo, portGo := testServer(t, GOLANG_SFTP, READONLY) defer listenerGo.Close()