浏览代码

sftp realpath: resolve symlinks

Fixes #890

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 年之前
父节点
当前提交
55b47cf741
共有 14 个文件被更改,包括 244 次插入100 次删除
  1. 4 1
      README.md
  2. 1 1
      common/connection.go
  3. 1 1
      dataprovider/user.go
  4. 6 2
      docs/service.md
  5. 8 7
      go.mod
  6. 16 18
      go.sum
  7. 25 0
      sftpd/handler.go
  8. 1 1
      sftpd/server.go
  9. 109 60
      sftpd/sftpd_test.go
  10. 1 1
      templates/webadmin/adminsetup.html
  11. 7 1
      util/util.go
  12. 36 4
      vfs/osfs.go
  13. 23 3
      vfs/sftpfs.go
  14. 6 0
      vfs/vfs.go

+ 4 - 1
README.md

@@ -14,8 +14,11 @@ Several storage backends are supported: local filesystem, encrypted local filesy
 ## Sponsors
 ## Sponsors
 
 
 If you find SFTPGo useful please consider supporting this Open Source project.
 If you find SFTPGo useful please consider supporting this Open Source project.
-Maintaing and evolving SFTPGo is a lot of work - easily the equivalent of a full time job - for me.
+
+Maintaining and evolving SFTPGo is a lot of work - easily the equivalent of a full time job - for me.
+
 I'd like to make SFTPGo into a sustainable long term project and would not like to introduce a dual licensing option and limit some features to the proprietary version only.
 I'd like to make SFTPGo into a sustainable long term project and would not like to introduce a dual licensing option and limit some features to the proprietary version only.
+
 If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
 If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
 This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
 This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
 
 

+ 1 - 1
common/connection.go

@@ -482,7 +482,7 @@ func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) err
 	initialSize := int64(-1)
 	initialSize := int64(-1)
 	if dstInfo, err := fsDst.Lstat(fsTargetPath); err == nil {
 	if dstInfo, err := fsDst.Lstat(fsTargetPath); err == nil {
 		if dstInfo.IsDir() {
 		if dstInfo.IsDir() {
-			c.Log(logger.LevelWarn, "attempted to rename %#v overwriting an existing directory %#v",
+			c.Log(logger.LevelWarn, "attempted to rename %q overwriting an existing directory %q",
 				fsSourcePath, fsTargetPath)
 				fsSourcePath, fsTargetPath)
 			return c.GetOpUnsupportedError()
 			return c.GetOpUnsupportedError()
 		}
 		}

+ 1 - 1
dataprovider/user.go

@@ -198,7 +198,7 @@ func (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {
 func (u *User) checkRootPath(connectionID string) error {
 func (u *User) checkRootPath(connectionID string) error {
 	fs, err := u.GetFilesystemForPath("/", connectionID)
 	fs, err := u.GetFilesystemForPath("/", connectionID)
 	if err != nil {
 	if err != nil {
-		logger.Warn(logSender, connectionID, "could not create main filesystem for user %#v err: %v", u.Username, err)
+		logger.Warn(logSender, connectionID, "could not create main filesystem for user %q err: %v", u.Username, err)
 		return fmt.Errorf("could not create root filesystem: %w", err)
 		return fmt.Errorf("could not create root filesystem: %w", err)
 	}
 	}
 	fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
 	fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())

+ 6 - 2
docs/service.md

@@ -6,9 +6,9 @@ Run the following instructions from the directory that contains the sftpgo binar
 
 
 ## Linux
 ## Linux
 
 
-The easiest way to run SFTPGo as a service is to download and install the pre-compiled deb/rpm package or use one of the Arch Linux PKGBUILDs we maintain.
+The easiest way to run SFTPGo as a service is to use the [deb/rpm repos](./repo.md) or download and install a pre-compiled deb/rpm package or use one of the Arch Linux PKGBUILDs we maintain.
 
 
-This section describes the procedure to use if you prefer to build SFTPGo yourself or if you want to download and configure a pre-built release as tar.
+This section describes the manual procedure.
 
 
 A `systemd` sample [service](../init/sftpgo.service "systemd service") can be found inside the source tree.
 A `systemd` sample [service](../init/sftpgo.service "systemd service") can be found inside the source tree.
 
 
@@ -67,6 +67,10 @@ sudo /usr/bin/sftpgo gen man -d /usr/share/man/man1
 
 
 ## macOS
 ## macOS
 
 
+The easiest way to run SFTPGo as a service is to install it from the Homebrew [Formula](https://formulae.brew.sh/formula/sftpgo).
+
+This section describes the procedure to use if you prefer to build SFTPGo yourself or if you want to download and configure a pre-built release as tar.
+
 For macOS, a `launchd` sample [service](../init/com.github.drakkan.sftpgo.plist "launchd plist") can be found inside the source tree. The `launchd` plist assumes that SFTPGo has `/usr/local/opt/sftpgo` as base directory.
 For macOS, a `launchd` sample [service](../init/com.github.drakkan.sftpgo.plist "launchd plist") can be found inside the source tree. The `launchd` plist assumes that SFTPGo has `/usr/local/opt/sftpgo` as base directory.
 
 
 Here are some basic instructions to run SFTPGo as service, please run the following commands from the directory where you downloaded SFTPGo:
 Here are some basic instructions to run SFTPGo as service, please run the following commands from the directory where you downloaded SFTPGo:

+ 8 - 7
go.mod

@@ -35,7 +35,7 @@ require (
 	github.com/hashicorp/go-plugin v1.4.4
 	github.com/hashicorp/go-plugin v1.4.4
 	github.com/hashicorp/go-retryablehttp v0.7.1
 	github.com/hashicorp/go-retryablehttp v0.7.1
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
-	github.com/klauspost/compress v1.15.7
+	github.com/klauspost/compress v1.15.8
 	github.com/lestrrat-go/jwx v1.2.25
 	github.com/lestrrat-go/jwx v1.2.25
 	github.com/lib/pq v1.10.6
 	github.com/lib/pq v1.10.6
 	github.com/lithammer/shortuuid/v3 v3.0.7
 	github.com/lithammer/shortuuid/v3 v3.0.7
@@ -53,12 +53,12 @@ require (
 	github.com/rs/zerolog v1.27.0
 	github.com/rs/zerolog v1.27.0
 	github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
 	github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
 	github.com/shirou/gopsutil/v3 v3.22.6
 	github.com/shirou/gopsutil/v3 v3.22.6
-	github.com/spf13/afero v1.8.2
+	github.com/spf13/afero v1.9.0
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/viper v1.12.0
 	github.com/spf13/viper v1.12.0
 	github.com/stretchr/testify v1.8.0
 	github.com/stretchr/testify v1.8.0
 	github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
 	github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
-	github.com/unrolled/secure v1.11.0
+	github.com/unrolled/secure v1.12.0
 	github.com/wagslane/go-password-validator v0.3.0
 	github.com/wagslane/go-password-validator v0.3.0
 	github.com/xhit/go-simple-mail/v2 v2.11.0
 	github.com/xhit/go-simple-mail/v2 v2.11.0
 	github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
 	github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
@@ -68,7 +68,7 @@ require (
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
 	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
 	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
 	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
 	golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
 	golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
-	golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e
+	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858
 	google.golang.org/api v0.87.0
 	google.golang.org/api v0.87.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
@@ -103,7 +103,7 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-test/deep v1.0.8 // indirect
 	github.com/go-test/deep v1.0.8 // indirect
-	github.com/goccy/go-json v0.9.8 // indirect
+	github.com/goccy/go-json v0.9.10 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
@@ -138,7 +138,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
 	github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
-	github.com/prometheus/common v0.36.0 // indirect
+	github.com/prometheus/common v0.37.0 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
@@ -155,7 +155,7 @@ require (
 	golang.org/x/tools v0.1.11 // indirect
 	golang.org/x/tools v0.1.11 // indirect
 	golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
 	golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220712132514-bdd2acd4974d // indirect
+	google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9 // indirect
 	google.golang.org/grpc v1.48.0 // indirect
 	google.golang.org/grpc v1.48.0 // indirect
 	google.golang.org/protobuf v1.28.0 // indirect
 	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/ini.v1 v1.66.6 // indirect
 	gopkg.in/ini.v1 v1.66.6 // indirect
@@ -166,6 +166,7 @@ require (
 
 
 replace (
 replace (
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
 	github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
+	github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d
 	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220624105932-71c5dfcef1e8
 	golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220624105932-71c5dfcef1e8
 	golang.org/x/net => github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0
 	golang.org/x/net => github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0
 )
 )

+ 16 - 18
go.sum

@@ -264,6 +264,8 @@ github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHP
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
 github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
 github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0 h1:VL3ucjcGkcuAEflNvFbNYHOTdAqgYOMRJBBRy7nQ/E4=
 github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0 h1:VL3ucjcGkcuAEflNvFbNYHOTdAqgYOMRJBBRy7nQ/E4=
 github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d h1:kNk/KRhszPJASp7WvjagNW254aKK643Lu8/fr4/ukiM=
+github.com/drakkan/sftp v0.0.0-20220716075551-51a5aa4e044d/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -332,8 +334,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
-github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
+github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -540,8 +542,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok=
-github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/compress v1.15.8 h1:JahtItbkWjf2jzm/T+qgMxkP9EMHsqEUA6vCMGmXvhA=
+github.com/klauspost/compress v1.15.8/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
 github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
 github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
@@ -654,9 +656,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
-github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
@@ -681,8 +680,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/common v0.36.0 h1:78hJTing+BLYLjhXE+Z2BubeEymH5Lr0/Mt8FKkxxYo=
-github.com/prometheus/common v0.36.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
@@ -720,8 +719,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
-github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
+github.com/spf13/afero v1.9.0 h1:sFSLUHgxdnN32Qy38hK3QkYBFXZj9DKjVjCUCtD7juY=
+github.com/spf13/afero v1.9.0/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
@@ -760,8 +759,8 @@ github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjM
 github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
 github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/unrolled/secure v1.11.0 h1:fjkKhD/MsQnlmz/au+MmFptCFNhvf5iv04ALkdCXRCI=
-github.com/unrolled/secure v1.11.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
+github.com/unrolled/secure v1.12.0 h1:7k3jcgLwfjiKkhQde6VbQ3D4KDLtDBqDd/hs3PPANDY=
+github.com/unrolled/secure v1.12.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
 github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
 github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
 github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
 github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
 github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
 github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
@@ -935,7 +934,6 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -972,8 +970,8 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew=
-golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1224,8 +1222,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
 google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220712132514-bdd2acd4974d h1:YbuF5+kdiC516xIP60RvlHeFbY9sRDR73QsAGHpkeVw=
-google.golang.org/genproto v0.0.0-20220712132514-bdd2acd4974d/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
+google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9 h1:1aEQRgZ4Gks2SRAkLzIPpIszRazwVfjSFe1cKc+e0Jg=
+google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

+ 25 - 0
sftpd/handler.go

@@ -268,6 +268,31 @@ func (c *Connection) Lstat(request *sftp.Request) (sftp.ListerAt, error) {
 	return listerAt([]os.FileInfo{s}), nil
 	return listerAt([]os.FileInfo{s}), nil
 }
 }
 
 
+// RealPath implements the RealPathFileLister interface
+func (c *Connection) RealPath(p string) (string, error) {
+	if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(p)) {
+		return "", sftp.ErrSSHFxPermissionDenied
+	}
+
+	if c.User.Filters.StartDirectory == "" {
+		p = util.CleanPath(p)
+	} else {
+		p = util.CleanPathWithBase(c.User.Filters.StartDirectory, p)
+	}
+	fs, fsPath, err := c.GetFsAndResolvedPath(p)
+	if err != nil {
+		return "", err
+	}
+	if realPather, ok := fs.(vfs.FsRealPather); ok {
+		realPath, err := realPather.RealPath(fsPath)
+		if err != nil {
+			return "", c.GetFsError(fs, err)
+		}
+		return realPath, nil
+	}
+	return p, nil
+}
+
 // StatVFS implements StatVFSFileCmder interface
 // StatVFS implements StatVFSFileCmder interface
 func (c *Connection) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) {
 func (c *Connection) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) {
 	c.UpdateLastActivity()
 	c.UpdateLastActivity()

+ 1 - 1
sftpd/server.go

@@ -613,7 +613,7 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
 		sftp.WithStartDirectory(connection.User.Filters.StartDirectory))
 		sftp.WithStartDirectory(connection.User.Filters.StartDirectory))
 
 
 	defer server.Close()
 	defer server.Close()
-	if err := server.Serve(); err == io.EOF {
+	if err := server.Serve(); errors.Is(err, io.EOF) {
 		exitStatus := sshSubsystemExitStatus{Status: uint32(0)}
 		exitStatus := sshSubsystemExitStatus{Status: uint32(0)}
 		_, err = channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
 		_, err = channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
 		connection.Log(logger.LevelInfo, "connection closed, sent exit status %+v error: %v", exitStatus, err)
 		connection.Log(logger.LevelInfo, "connection closed, sent exit status %+v error: %v", exitStatus, err)

+ 109 - 60
sftpd/sftpd_test.go

@@ -763,60 +763,70 @@ func TestStartDirectory(t *testing.T) {
 	startDir := "/st@ rt/dir"
 	startDir := "/st@ rt/dir"
 	u := getTestUser(usePubKey)
 	u := getTestUser(usePubKey)
 	u.Filters.StartDirectory = startDir
 	u.Filters.StartDirectory = startDir
-	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	conn, client, err := getSftpClient(user, usePubKey)
-	if assert.NoError(t, err) {
-		defer conn.Close()
-		defer client.Close()
+	u = getTestSFTPUser(usePubKey)
+	u.Filters.StartDirectory = startDir
+	sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+	for _, user := range []dataprovider.User{localUser, sftpUser} {
+		conn, client, err := getSftpClient(user, usePubKey)
+		if assert.NoError(t, err) {
+			defer conn.Close()
+			defer client.Close()
 
 
-		currentDir, err := client.Getwd()
-		assert.NoError(t, err)
-		assert.Equal(t, startDir, currentDir)
+			currentDir, err := client.Getwd()
+			assert.NoError(t, err)
+			assert.Equal(t, startDir, currentDir)
 
 
-		entries, err := client.ReadDir(".")
-		assert.NoError(t, err)
-		assert.Len(t, entries, 0)
+			entries, err := client.ReadDir(".")
+			assert.NoError(t, err)
+			assert.Len(t, entries, 0)
 
 
-		testFilePath := filepath.Join(homeBasePath, testFileName)
-		testFileSize := int64(65535)
-		err = createTestFile(testFilePath, testFileSize)
-		assert.NoError(t, err)
-		err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
-		assert.NoError(t, err)
-		localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
-		err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
-		assert.NoError(t, err)
-		_, err = client.Stat(testFileName)
-		assert.NoError(t, err)
-		err = client.Rename(testFileName, testFileName+"_rename")
-		assert.NoError(t, err)
+			testFilePath := filepath.Join(homeBasePath, testFileName)
+			testFileSize := int64(65535)
+			err = createTestFile(testFilePath, testFileSize)
+			assert.NoError(t, err)
+			err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
+			assert.NoError(t, err)
+			localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
+			err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
+			assert.NoError(t, err)
+			_, err = client.Stat(testFileName)
+			assert.NoError(t, err)
+			err = client.Rename(testFileName, testFileName+"_rename")
+			assert.NoError(t, err)
 
 
-		entries, err = client.ReadDir(".")
-		assert.NoError(t, err)
-		assert.Len(t, entries, 1)
+			entries, err = client.ReadDir(".")
+			assert.NoError(t, err)
+			assert.Len(t, entries, 1)
 
 
-		currentDir, err = client.RealPath("..")
-		assert.NoError(t, err)
-		assert.Equal(t, path.Dir(startDir), currentDir)
+			currentDir, err = client.RealPath("..")
+			assert.NoError(t, err)
+			assert.Equal(t, path.Dir(startDir), currentDir)
 
 
-		currentDir, err = client.RealPath("../..")
-		assert.NoError(t, err)
-		assert.Equal(t, "/", currentDir)
+			currentDir, err = client.RealPath("../..")
+			assert.NoError(t, err)
+			assert.Equal(t, "/", currentDir)
 
 
-		currentDir, err = client.RealPath("../../..")
-		assert.NoError(t, err)
-		assert.Equal(t, "/", currentDir)
+			currentDir, err = client.RealPath("../../..")
+			assert.NoError(t, err)
+			assert.Equal(t, "/", currentDir)
 
 
-		err = os.Remove(testFilePath)
-		assert.NoError(t, err)
-		err = os.Remove(localDownloadPath)
-		assert.NoError(t, err)
-	}
+			err = client.Remove(testFileName + "_rename")
+			assert.NoError(t, err)
 
 
-	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+			err = os.Remove(testFilePath)
+			assert.NoError(t, err)
+			err = os.Remove(localDownloadPath)
+			assert.NoError(t, err)
+		}
+	}
+	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	err = os.RemoveAll(user.GetHomeDir())
+	_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
+	assert.NoError(t, err)
+	err = os.RemoveAll(localUser.GetHomeDir())
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 }
 }
 
 
@@ -1181,22 +1191,59 @@ func TestProxyProtocol(t *testing.T) {
 func TestRealPath(t *testing.T) {
 func TestRealPath(t *testing.T) {
 	usePubKey := true
 	usePubKey := true
 	u := getTestUser(usePubKey)
 	u := getTestUser(usePubKey)
-	user, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	conn, client, err := getSftpClient(user, usePubKey)
-	if assert.NoError(t, err) {
-		defer conn.Close()
-		defer client.Close()
-		p, err := client.RealPath("../..")
-		assert.NoError(t, err)
-		assert.Equal(t, "/", p)
-		p, err = client.RealPath("../test")
-		assert.NoError(t, err)
-		assert.Equal(t, "/test", p)
+	u = getTestSFTPUser(usePubKey)
+	sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+	for _, user := range []dataprovider.User{localUser, sftpUser} {
+		conn, client, err := getSftpClient(user, usePubKey)
+		if assert.NoError(t, err) {
+			defer conn.Close()
+			defer client.Close()
+
+			p, err := client.RealPath("../..")
+			assert.NoError(t, err)
+			assert.Equal(t, "/", p)
+			p, err = client.RealPath("../test")
+			assert.NoError(t, err)
+			assert.Equal(t, "/test", p)
+			subdir := "testsubdir"
+			err = client.Mkdir(subdir)
+			assert.NoError(t, err)
+			linkName := testFileName + "_link"
+			err = client.Symlink(testFileName, path.Join(subdir, linkName))
+			assert.NoError(t, err)
+			p, err = client.RealPath(path.Join(subdir, linkName))
+			assert.NoError(t, err)
+			assert.Equal(t, path.Join("/", testFileName), p)
+			// an existing path
+			sftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
+			if assert.NoError(t, err) {
+				testData := []byte("hello world")
+				n, err := sftpFile.WriteAt(testData, 0)
+				assert.NoError(t, err)
+				assert.Equal(t, len(testData), n)
+			}
+			p, err = client.RealPath(path.Join(subdir, linkName))
+			assert.NoError(t, err)
+			assert.Equal(t, path.Join("/", testFileName), p)
+			// now a link outside the home dir
+			err = os.Symlink(filepath.Clean(os.TempDir()), filepath.Join(localUser.GetHomeDir(), subdir, "temp"))
+			assert.NoError(t, err)
+			_, err = client.RealPath(path.Join(subdir, "temp"))
+			assert.ErrorIs(t, err, os.ErrPermission)
+			err = os.Remove(filepath.Join(localUser.GetHomeDir(), subdir, "temp"))
+			assert.NoError(t, err)
+			err = os.RemoveAll(filepath.Join(localUser.GetHomeDir(), subdir))
+			assert.NoError(t, err)
+		}
 	}
 	}
-	_, err = httpdtest.RemoveUser(user, http.StatusOK)
+	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	err = os.RemoveAll(user.GetHomeDir())
+	_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
+	assert.NoError(t, err)
+	err = os.RemoveAll(localUser.GetHomeDir())
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 }
 }
 
 
@@ -7138,13 +7185,15 @@ func TestPermList(t *testing.T) {
 		defer conn.Close()
 		defer conn.Close()
 		defer client.Close()
 		defer client.Close()
 		_, err = client.ReadDir(".")
 		_, err = client.ReadDir(".")
-		assert.Error(t, err, "read remote dir without permission should not succeed")
+		assert.ErrorIs(t, err, os.ErrPermission, "read remote dir without permission should not succeed")
 		_, err = client.Stat("test_file")
 		_, err = client.Stat("test_file")
-		assert.Error(t, err, "stat remote file without permission should not succeed")
+		assert.ErrorIs(t, err, os.ErrPermission, "stat remote file without permission should not succeed")
 		_, err = client.Lstat("test_file")
 		_, err = client.Lstat("test_file")
-		assert.Error(t, err, "lstat remote file without permission should not succeed")
+		assert.ErrorIs(t, err, os.ErrPermission, "lstat remote file without permission should not succeed")
 		_, err = client.ReadLink("test_link")
 		_, err = client.ReadLink("test_link")
-		assert.Error(t, err, "read remote link without permission on source dir should not succeed")
+		assert.ErrorIs(t, err, os.ErrPermission, "read remote link without permission on source dir should not succeed")
+		_, err = client.RealPath(".")
+		assert.ErrorIs(t, err, os.ErrPermission, "real path without permission should not succeed")
 		f, err := client.Create(testFileName)
 		f, err := client.Create(testFileName)
 		if assert.NoError(t, err) {
 		if assert.NoError(t, err) {
 			_, err = f.Write([]byte("content"))
 			_, err = f.Write([]byte("content"))

+ 1 - 1
templates/webadmin/adminsetup.html

@@ -72,7 +72,7 @@
                                     {{if not .HideSupportLink}}
                                     {{if not .HideSupportLink}}
                                     <hr>
                                     <hr>
                                     <div class="text-center">
                                     <div class="text-center">
-                                        <a class="small" href="https://github.com/drakkan/sftpgo#sponsors">SFTPGo needs your help</a>
+                                        <a class="small" href="https://github.com/drakkan/sftpgo#sponsors" target="_blank">SFTPGo needs your help</a>
                                     </div>
                                     </div>
                                     {{end}}
                                     {{end}}
                                 </div>
                                 </div>

+ 7 - 1
util/util.go

@@ -308,9 +308,15 @@ func GetDirsForVirtualPath(virtualPath string) []string {
 
 
 // CleanPath returns a clean POSIX (/) absolute path to work with
 // CleanPath returns a clean POSIX (/) absolute path to work with
 func CleanPath(p string) string {
 func CleanPath(p string) string {
+	return CleanPathWithBase("/", p)
+}
+
+// CleanPathWithBase returns a clean POSIX (/) absolute path to work with.
+// The specified base will be used if the provided path is not absolute
+func CleanPathWithBase(base, p string) string {
 	p = filepath.ToSlash(p)
 	p = filepath.ToSlash(p)
 	if !path.IsAbs(p) {
 	if !path.IsAbs(p) {
-		p = "/" + p
+		p = path.Join(base, p)
 	}
 	}
 	return path.Clean(p)
 	return path.Clean(p)
 }
 }

+ 36 - 4
vfs/osfs.go

@@ -313,12 +313,44 @@ func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
 
 
 	err = fs.isSubDir(p)
 	err = fs.isSubDir(p)
 	if err != nil {
 	if err != nil {
-		fsLog(fs, logger.LevelError, "Invalid path resolution, dir %#v original path %#v resolved %#v err: %v",
+		fsLog(fs, logger.LevelError, "Invalid path resolution, path %q original path %q resolved %q err: %v",
 			p, virtualPath, r, err)
 			p, virtualPath, r, err)
 	}
 	}
 	return r, err
 	return r, err
 }
 }
 
 
+// RealPath implements the FsRealPather interface
+func (fs *OsFs) RealPath(p string) (string, error) {
+	linksWalked := 0
+	for {
+		info, err := os.Lstat(p)
+		if err != nil {
+			if errors.Is(err, os.ErrNotExist) {
+				return fs.GetRelativePath(p), nil
+			}
+			return "", err
+		}
+		if info.Mode()&os.ModeSymlink == 0 {
+			return fs.GetRelativePath(p), nil
+		}
+		resolvedLink, err := os.Readlink(p)
+		if err != nil {
+			return "", err
+		}
+		resolvedLink = filepath.Clean(resolvedLink)
+		if filepath.IsAbs(resolvedLink) {
+			p = resolvedLink
+		} else {
+			p = filepath.Join(filepath.Dir(p), resolvedLink)
+		}
+		linksWalked++
+		if linksWalked > 10 {
+			fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
+			return "", &pathResolutionError{err: "too many links"}
+		}
+	}
+}
+
 // GetDirSize returns the number of files and the size for a folder
 // GetDirSize returns the number of files and the size for a folder
 // including any subfolders
 // including any subfolders
 func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
 func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
@@ -402,14 +434,14 @@ func (fs *OsFs) isSubDir(sub string) error {
 	// fs.rootDir must exist and it is already a validated absolute path
 	// fs.rootDir must exist and it is already a validated absolute path
 	parent, err := filepath.EvalSymlinks(fs.rootDir)
 	parent, err := filepath.EvalSymlinks(fs.rootDir)
 	if err != nil {
 	if err != nil {
-		fsLog(fs, logger.LevelError, "invalid root path %#v: %v", fs.rootDir, err)
+		fsLog(fs, logger.LevelError, "invalid root path %q: %v", fs.rootDir, err)
 		return err
 		return err
 	}
 	}
 	if parent == sub {
 	if parent == sub {
 		return nil
 		return nil
 	}
 	}
 	if len(sub) < len(parent) {
 	if len(sub) < len(parent) {
-		err = fmt.Errorf("path %#v is not inside %#v", sub, parent)
+		err = fmt.Errorf("path %q is not inside %q", sub, parent)
 		return &pathResolutionError{err: err.Error()}
 		return &pathResolutionError{err: err.Error()}
 	}
 	}
 	separator := string(os.PathSeparator)
 	separator := string(os.PathSeparator)
@@ -419,7 +451,7 @@ func (fs *OsFs) isSubDir(sub string) error {
 		separator = ""
 		separator = ""
 	}
 	}
 	if !strings.HasPrefix(sub, parent+separator) {
 	if !strings.HasPrefix(sub, parent+separator) {
-		err = fmt.Errorf("path %#v is not inside %#v", sub, parent)
+		err = fmt.Errorf("path %q is not inside %q", sub, parent)
 		return &pathResolutionError{err: err.Error()}
 		return &pathResolutionError{err: err.Error()}
 	}
 	}
 	return nil
 	return nil

+ 23 - 3
vfs/sftpfs.go

@@ -628,6 +628,25 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
 	return fsPath, nil
 	return fsPath, nil
 }
 }
 
 
+// RealPath implements the FsRealPather interface
+func (fs *SFTPFs) RealPath(p string) (string, error) {
+	if err := fs.checkConnection(); err != nil {
+		return "", err
+	}
+	resolved, err := fs.sftpClient.RealPath(p)
+	if err != nil {
+		return "", err
+	}
+	if fs.config.Prefix != "/" {
+		if err := fs.isSubDir(resolved); err != nil {
+			fsLog(fs, logger.LevelError, "Invalid real path resolution, original path %q resolved %q err: %v",
+				p, resolved, err)
+			return "", err
+		}
+	}
+	return fs.GetRelativePath(resolved), nil
+}
+
 // getRealPath returns the real remote path trying to resolve symbolic links if any
 // getRealPath returns the real remote path trying to resolve symbolic links if any
 func (fs *SFTPFs) getRealPath(name string) (string, error) {
 func (fs *SFTPFs) getRealPath(name string) (string, error) {
 	linksWalked := 0
 	linksWalked := 0
@@ -641,7 +660,7 @@ func (fs *SFTPFs) getRealPath(name string) (string, error) {
 		}
 		}
 		resolvedLink, err := fs.sftpClient.ReadLink(name)
 		resolvedLink, err := fs.sftpClient.ReadLink(name)
 		if err != nil {
 		if err != nil {
-			return name, err
+			return name, fmt.Errorf("unable to resolve link to %q: %w", name, err)
 		}
 		}
 		resolvedLink = path.Clean(resolvedLink)
 		resolvedLink = path.Clean(resolvedLink)
 		if path.IsAbs(resolvedLink) {
 		if path.IsAbs(resolvedLink) {
@@ -651,6 +670,7 @@ func (fs *SFTPFs) getRealPath(name string) (string, error) {
 		}
 		}
 		linksWalked++
 		linksWalked++
 		if linksWalked > 10 {
 		if linksWalked > 10 {
+			fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
 			return "", &pathResolutionError{err: "too many links"}
 			return "", &pathResolutionError{err: "too many links"}
 		}
 		}
 	}
 	}
@@ -661,11 +681,11 @@ func (fs *SFTPFs) isSubDir(name string) error {
 		return nil
 		return nil
 	}
 	}
 	if len(name) < len(fs.config.Prefix) {
 	if len(name) < len(fs.config.Prefix) {
-		err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
+		err := fmt.Errorf("path %q is not inside: %#v", name, fs.config.Prefix)
 		return &pathResolutionError{err: err.Error()}
 		return &pathResolutionError{err: err.Error()}
 	}
 	}
 	if !strings.HasPrefix(name, fs.config.Prefix+"/") {
 	if !strings.HasPrefix(name, fs.config.Prefix+"/") {
-		err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
+		err := fmt.Errorf("path %q is not inside: %#v", name, fs.config.Prefix)
 		return &pathResolutionError{err: err.Error()}
 		return &pathResolutionError{err: err.Error()}
 	}
 	}
 	return nil
 	return nil

+ 6 - 0
vfs/vfs.go

@@ -89,6 +89,12 @@ type Fs interface {
 	Close() error
 	Close() error
 }
 }
 
 
+// FsRealPather is a Fs that implements the RealPath method.
+type FsRealPather interface {
+	Fs
+	RealPath(p string) (string, error)
+}
+
 // fsMetadataChecker is a Fs that implements the getFileNamesInPrefix method.
 // fsMetadataChecker is a Fs that implements the getFileNamesInPrefix method.
 // This interface is used to abstract metadata consistency checks
 // This interface is used to abstract metadata consistency checks
 type fsMetadataChecker interface {
 type fsMetadataChecker interface {