sftp realpath: resolve symlinks

Fixes #890

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-07-17 16:02:45 +02:00
parent e0ce2e2e8a
commit 55b47cf741
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
14 changed files with 244 additions and 100 deletions

View file

@ -14,8 +14,11 @@ Several storage backends are supported: local filesystem, encrypted local filesy
## Sponsors
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.
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:

View file

@ -482,7 +482,7 @@ func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) err
initialSize := int64(-1)
if dstInfo, err := fsDst.Lstat(fsTargetPath); err == nil {
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)
return c.GetOpUnsupportedError()
}

View file

@ -198,7 +198,7 @@ func (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {
func (u *User) checkRootPath(connectionID string) error {
fs, err := u.GetFilesystemForPath("/", connectionID)
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)
}
fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())

View file

@ -6,9 +6,9 @@ Run the following instructions from the directory that contains the sftpgo binar
## 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.
@ -67,6 +67,10 @@ sudo /usr/bin/sftpgo gen man -d /usr/share/man/man1
## 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.
Here are some basic instructions to run SFTPGo as service, please run the following commands from the directory where you downloaded SFTPGo:

15
go.mod
View file

@ -35,7 +35,7 @@ require (
github.com/hashicorp/go-plugin v1.4.4
github.com/hashicorp/go-retryablehttp v0.7.1
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/lib/pq v1.10.6
github.com/lithammer/shortuuid/v3 v3.0.7
@ -53,12 +53,12 @@ require (
github.com/rs/zerolog v1.27.0
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
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/viper v1.12.0
github.com/stretchr/testify v1.8.0
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/xhit/go-simple-mail/v2 v2.11.0
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/net v0.0.0-20220624214902-1bab6f366d9e
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
google.golang.org/api v0.87.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@ -103,7 +103,7 @@ require (
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-ole/go-ole v1.2.6 // 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/protobuf v1.5.2 // 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/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // 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/russross/blackfriday/v2 v2.1.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/xerrors v0.0.0-20220609144429-65e65417b02f // 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/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
@ -166,6 +166,7 @@ require (
replace (
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/net => github.com/drakkan/net v0.0.0-20220628171916-78de6a2a21b0
)

34
go.sum
View file

@ -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/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/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/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
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/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.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/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
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/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.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.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.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.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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
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.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
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/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
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/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/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/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
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-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-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-20210510120138-977fb7262007/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-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-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-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
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-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-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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

View file

@ -268,6 +268,31 @@ func (c *Connection) Lstat(request *sftp.Request) (sftp.ListerAt, error) {
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
func (c *Connection) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) {
c.UpdateLastActivity()

View file

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

View file

@ -763,60 +763,70 @@ func TestStartDirectory(t *testing.T) {
startDir := "/st@ rt/dir"
u := getTestUser(usePubKey)
u.Filters.StartDirectory = startDir
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
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 = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
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)
}
@ -1181,22 +1191,59 @@ func TestProxyProtocol(t *testing.T) {
func TestRealPath(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
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)
err = os.RemoveAll(user.GetHomeDir())
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
}
@ -7138,13 +7185,15 @@ func TestPermList(t *testing.T) {
defer conn.Close()
defer client.Close()
_, 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")
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")
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")
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)
if assert.NoError(t, err) {
_, err = f.Write([]byte("content"))

View file

@ -72,7 +72,7 @@
{{if not .HideSupportLink}}
<hr>
<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>
{{end}}
</div>

View file

@ -308,9 +308,15 @@ func GetDirsForVirtualPath(virtualPath string) []string {
// CleanPath returns a clean POSIX (/) absolute path to work with
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)
if !path.IsAbs(p) {
p = "/" + p
p = path.Join(base, p)
}
return path.Clean(p)
}

View file

@ -313,12 +313,44 @@ func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
err = fs.isSubDir(p)
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)
}
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
// including any subfolders
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
parent, err := filepath.EvalSymlinks(fs.rootDir)
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
}
if parent == sub {
return nil
}
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()}
}
separator := string(os.PathSeparator)
@ -419,7 +451,7 @@ func (fs *OsFs) isSubDir(sub string) error {
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 nil

View file

@ -628,6 +628,25 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
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
func (fs *SFTPFs) getRealPath(name string) (string, error) {
linksWalked := 0
@ -641,7 +660,7 @@ func (fs *SFTPFs) getRealPath(name string) (string, error) {
}
resolvedLink, err := fs.sftpClient.ReadLink(name)
if err != nil {
return name, err
return name, fmt.Errorf("unable to resolve link to %q: %w", name, err)
}
resolvedLink = path.Clean(resolvedLink)
if path.IsAbs(resolvedLink) {
@ -651,6 +670,7 @@ func (fs *SFTPFs) getRealPath(name string) (string, error) {
}
linksWalked++
if linksWalked > 10 {
fsLog(fs, logger.LevelError, "unable to get real path, too many links: %d", linksWalked)
return "", &pathResolutionError{err: "too many links"}
}
}
@ -661,11 +681,11 @@ func (fs *SFTPFs) isSubDir(name string) error {
return nil
}
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()}
}
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 nil

View file

@ -89,6 +89,12 @@ type Fs interface {
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.
// This interface is used to abstract metadata consistency checks
type fsMetadataChecker interface {