Repository: pkg/sftp
Pull request #437 (2021-04-27 09:13:09)

request server: add an optional RealpathFileLister interface
This allow to customize the responses for SSH_FXP_REALPATH requests
and so implementing features like a start directory.

I'm not sure if this is the best way to add this feature, maybe we could consider a server option too


Patch:
diff --git a/request-example.go b/request-example.go
index ab6c675..ba22bcd 100644
--- a/request-example.go
+++ b/request-example.go
@@ -464,10 +464,19 @@ func (fs *root) Lstat(r *Request) (ListerAt, error) {
 	return listerat{file}, nil
 }
 
+// implements RealpathFileLister interface
+func (fs *root) Realpath(p string) string {
+	if fs.startDirectory == "" || fs.startDirectory == "/" {
+		return cleanPath(p)
+	}
+	return cleanPathWithBase(fs.startDirectory, p)
+}
+
 // In memory file-system-y thing that the Hanlders live on
 type root struct {
-	rootFile *memFile
-	mockErr  error
+	rootFile       *memFile
+	mockErr        error
+	startDirectory string
 
 	mu    sync.Mutex
 	files map[string]*memFile
diff --git a/request-interfaces.go b/request-interfaces.go
index 4529d99..41e3327 100644
--- a/request-interfaces.go
+++ b/request-interfaces.go
@@ -86,6 +86,15 @@ type LstatFileLister interface {
 	Lstat(*Request) (ListerAt, error)
 }
 
+// RealPathFileLister is a FileLister that implements the Realpath method.
+// We use "/" as start directory for relative paths, implementing this
+// interface you can customize the start directory.
+// You have to return an absolute POSIX path.
+type RealPathFileLister interface {
+	FileLister
+	RealPath(string) string
+}
+
 // ListerAt does for file lists what io.ReaderAt does for files.
 // ListAt should return the number of entries copied and an io.EOF
 // error if at end of list. This is testable by comparing how many you
diff --git a/request-server.go b/request-server.go
index 0caafc9..b58aefc 100644
--- a/request-server.go
+++ b/request-server.go
@@ -198,7 +198,13 @@ func (rs *RequestServer) packetWorker(
 			handle := pkt.getHandle()
 			rpkt = statusFromError(pkt.ID, rs.closeRequest(handle))
 		case *sshFxpRealpathPacket:
-			rpkt = cleanPacketPath(pkt)
+			var realPath string
+			if realPather, ok := rs.Handlers.FileList.(RealPathFileLister); ok {
+				realPath = realPather.RealPath(pkt.getPath())
+			} else {
+				realPath = cleanPath(pkt.getPath())
+			}
+			rpkt = cleanPacketPath(pkt, realPath)
 		case *sshFxpOpendirPacket:
 			request := requestFromPacket(ctx, pkt)
 			handle := rs.nextRequest(request)
@@ -263,14 +269,13 @@ func (rs *RequestServer) packetWorker(
 }
 
 // clean and return name packet for file
-func cleanPacketPath(pkt *sshFxpRealpathPacket) responsePacket {
-	path := cleanPath(pkt.getPath())
+func cleanPacketPath(pkt *sshFxpRealpathPacket, realPath string) responsePacket {
 	return &sshFxpNamePacket{
 		ID: pkt.id(),
 		NameAttrs: []*sshFxpNameAttr{
 			{
-				Name:     path,
-				LongName: path,
+				Name:     realPath,
+				LongName: realPath,
 				Attrs:    emptyFileStat,
 			},
 		},
@@ -279,9 +284,13 @@ func cleanPacketPath(pkt *sshFxpRealpathPacket) responsePacket {
 
 // Makes sure we have a clean POSIX (/) absolute path to work with
 func cleanPath(p string) string {
+	return cleanPathWithBase("/", p)
+}
+
+func cleanPathWithBase(base, p string) string {
 	p = filepath.ToSlash(p)
 	if !path.IsAbs(p) {
-		p = "/" + p
+		return path.Join(base, p)
 	}
 	return path.Clean(p)
 }


Test patch:
diff --git a/request-server_test.go b/request-server_test.go
index 3d2502a..62cf311 100644
--- a/request-server_test.go
+++ b/request-server_test.go
@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"net"
 	"os"
+	"path"
 	"runtime"
 	"testing"
 	"time"
@@ -807,6 +808,25 @@ func TestUncleanDisconnect(t *testing.T) {
 	checkRequestServerAllocator(t, p)
 }
 
+func TestRealPath(t *testing.T) {
+	root := &root{
+		rootFile:       &memFile{name: "/", modtime: time.Now(), isdir: true},
+		files:          make(map[string]*memFile),
+		startDirectory: "/apath",
+	}
+
+	p := root.Realpath(".")
+	assert.Equal(t, root.startDirectory, p)
+	p = root.Realpath("/")
+	assert.Equal(t, "/", p)
+	p = root.Realpath("..")
+	assert.Equal(t, "/", p)
+	p = root.Realpath("../../..")
+	assert.Equal(t, "/", p)
+	p = root.Realpath("relpath")
+	assert.Equal(t, path.Join(root.startDirectory, "relpath"), p)
+}
+
 func TestCleanPath(t *testing.T) {
 	assert.Equal(t, "/", cleanPath("/"))
 	assert.Equal(t, "/", cleanPath("."))