add support for a start directory

Fixes #705

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-03-03 12:44:56 +01:00
parent 4519bffa39
commit 5c2fd8d52a
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
28 changed files with 478 additions and 94 deletions

View file

@ -20,12 +20,12 @@ jobs:
upload-coverage: false
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
@ -218,10 +218,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17
@ -274,10 +274,10 @@ jobs:
- 3307:3306
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17
@ -345,12 +345,12 @@ jobs:
go: latest
go-arch: arm7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
if: ${{ matrix.arch == 'amd64' }}
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
@ -449,10 +449,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: 1.17
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:

View file

@ -30,7 +30,7 @@ jobs:
optional_deps: false
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Gather image information
id: info

View file

@ -5,16 +5,16 @@ on:
tags: 'v*'
env:
GO_VERSION: 1.17.5
GO_VERSION: 1.17.7
jobs:
prepare-sources-with-deps:
name: Prepare sources with deps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@ -45,9 +45,9 @@ jobs:
os: [macos-10.15, windows-2019]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@ -283,10 +283,10 @@ jobs:
tar-arch: armv7
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Go
if: ${{ matrix.arch == 'amd64' }}
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
@ -467,7 +467,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Get versions
id: get_version
run: |

View file

@ -29,6 +29,7 @@ var (
portableAdvertiseCredentials bool
portableUsername string
portablePassword string
portableStartDir string
portableLogFile string
portableLogVerbose bool
portableLogUTCTime bool
@ -164,6 +165,7 @@ Please take a look at the usage below to customize the serving parameters`,
Filters: dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{
FilePatterns: parsePatternsFilesFilters(),
StartDirectory: portableStartDir,
},
},
FsConfig: vfs.Filesystem{
@ -246,6 +248,9 @@ func init() {
This can be an absolute path or a path
relative to the current directory
`)
portableCmd.Flags().StringVar(&portableStartDir, "start-directory", "/", `Alternate start directory.
This is a virtual path not a filesystem
path`)
portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, `0 means a random unprivileged port,
< 0 disabled`)
portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,

View file

@ -884,6 +884,7 @@ func TestGetTLSVersion(t *testing.T) {
func TestCleanPath(t *testing.T) {
assert.Equal(t, "/", util.CleanPath("/"))
assert.Equal(t, "/", util.CleanPath("."))
assert.Equal(t, "/", util.CleanPath(""))
assert.Equal(t, "/", util.CleanPath("/."))
assert.Equal(t, "/", util.CleanPath("/a/.."))
assert.Equal(t, "/a", util.CleanPath("/a/"))

View file

@ -2021,6 +2021,18 @@ func validateTransferLimitsFilter(user *User) error {
return nil
}
func updateFiltersValues(user *User) {
if !user.HasExternalAuth() {
user.Filters.ExternalAuthCacheTime = 0
}
if user.Filters.StartDirectory != "" {
user.Filters.StartDirectory = util.CleanPath(user.Filters.StartDirectory)
if user.Filters.StartDirectory == "/" {
user.Filters.StartDirectory = ""
}
}
}
func validateFilters(user *User) error {
checkEmptyFiltersStruct(user)
if err := validateIPFilters(user); err != nil {
@ -2061,9 +2073,7 @@ func validateFilters(user *User) error {
return util.NewValidationError(fmt.Sprintf("invalid web client options %#v", opts))
}
}
if !user.HasExternalAuth() {
user.Filters.ExternalAuthCacheTime = 0
}
updateFiltersValues(user)
return validateFiltersPatternExtensions(user)
}

View file

@ -219,6 +219,13 @@ func (u *User) CheckFsRoot(connectionID string) error {
return err
}
fs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
if u.Filters.StartDirectory != "" {
err = u.checkDirWithParents(u.Filters.StartDirectory, connectionID)
if err != nil {
logger.Warn(logSender, connectionID, "could not create start directory %#v, err: %v",
u.Filters.StartDirectory, err)
}
}
for idx := range u.VirtualFolders {
v := &u.VirtualFolders[idx]
fs, err = u.GetFilesystemForPath(v.VirtualPath, connectionID)
@ -234,6 +241,23 @@ func (u *User) CheckFsRoot(connectionID string) error {
return nil
}
// GetCleanedPath returns a clean POSIX absolute path using the user start directory as base
// if the provided rawVirtualPath is relative
func (u *User) GetCleanedPath(rawVirtualPath string) string {
if u.Filters.StartDirectory != "" {
if !path.IsAbs(rawVirtualPath) {
var b strings.Builder
b.Grow(len(u.Filters.StartDirectory) + 1 + len(rawVirtualPath))
b.WriteString(u.Filters.StartDirectory)
b.WriteString("/")
b.WriteString(rawVirtualPath)
return util.CleanPath(b.String())
}
}
return util.CleanPath(rawVirtualPath)
}
// isFsEqual returns true if the fs has the same configuration
func (u *User) isFsEqual(other *User) bool {
if u.FsConfig.Provider == sdk.LocalFilesystemProvider && u.GetHomeDir() != other.GetHomeDir() {
@ -242,6 +266,9 @@ func (u *User) isFsEqual(other *User) bool {
if !u.FsConfig.IsEqual(&other.FsConfig) {
return false
}
if u.Filters.StartDirectory != other.Filters.StartDirectory {
return false
}
if len(u.VirtualFolders) != len(other.VirtualFolders) {
return false
}
@ -586,13 +613,30 @@ func (u *User) GetVirtualFoldersInPath(virtualPath string) map[string]bool {
}
}
if u.Filters.StartDirectory != "" {
dirsForPath := util.GetDirsForVirtualPath(u.Filters.StartDirectory)
for index := range dirsForPath {
d := dirsForPath[index]
if d == "/" {
continue
}
if path.Dir(d) == virtualPath {
result[d] = true
}
}
}
return result
}
func (u *User) hasVirtualDirs() bool {
return len(u.VirtualFolders) > 0 || u.Filters.StartDirectory != ""
}
// FilterListDir adds virtual folders and remove hidden items from the given files list
func (u *User) FilterListDir(dirContents []os.FileInfo, virtualPath string) []os.FileInfo {
filter := u.getPatternsFilterForPath(virtualPath)
if len(u.VirtualFolders) == 0 && filter.DenyPolicy != sdk.DenyPolicyHide {
if !u.hasVirtualDirs() && filter.DenyPolicy != sdk.DenyPolicyHide {
return dirContents
}
@ -1395,6 +1439,7 @@ func (u *User) getACopy() User {
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
filters.DisableFsChecks = u.Filters.DisableFsChecks
filters.StartDirectory = u.Filters.StartDirectory
filters.AllowAPIKeyAuth = u.Filters.AllowAPIKeyAuth
filters.ExternalAuthCacheTime = u.Filters.ExternalAuthCacheTime
filters.WebClient = make([]string, len(u.Filters.WebClient))

View file

@ -2,7 +2,7 @@
The built-in `defender` allows you to configure an auto-blocking policy for SFTPGo and thus helps to prevent DoS (Denial of Service) and brute force password guessing.
If enabled it will protect SFTP, FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.
If enabled it will protect SFTP, HTTP, FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.
You can configure a score for the following events:

View file

@ -126,6 +126,9 @@ Flags:
"*" means any supported SSH command
including scp
(default [md5sum,sha1sum,cd,pwd,scp])
--start-directory string Alternate start directory.
This is a virtual path not a filesystem
path (default "/")
-u, --username string Leave empty to use an auto generated
value
--webdav-cert string Path to the certificate file for WebDAV

View file

@ -597,6 +597,68 @@ func TestBasicFTPHandling(t *testing.T) {
50*time.Millisecond)
}
func TestStartDirectory(t *testing.T) {
startDir := "/start/dir"
u := getTestUser()
u.Filters.StartDirectory = startDir
localUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
u = getTestSFTPUser()
u.Filters.StartDirectory = startDir
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client, err := getFTPClient(user, true, nil)
if assert.NoError(t, err) {
currentDir, err := client.CurrentDir()
assert.NoError(t, err)
assert.Equal(t, startDir, currentDir)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
assert.NoError(t, err)
entries, err := client.List(".")
assert.NoError(t, err)
assert.Len(t, entries, 3)
entries, err = client.List("/")
assert.NoError(t, err)
assert.Len(t, entries, 2)
err = client.ChangeDirToParent()
assert.NoError(t, err)
currentDir, err = client.CurrentDir()
assert.NoError(t, err)
assert.Equal(t, path.Dir(startDir), currentDir)
err = client.ChangeDirToParent()
assert.NoError(t, err)
currentDir, err = client.CurrentDir()
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.Quit()
assert.NoError(t, err)
}
}
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(localUser.GetHomeDir())
assert.NoError(t, err)
}
func TestMultiFactorAuth(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)

View file

@ -262,6 +262,8 @@ func (cc mockFTPClientContext) Path() string {
return ""
}
func (cc mockFTPClientContext) SetPath(name string) {}
func (cc mockFTPClientContext) SetDebug(debug bool) {}
func (cc mockFTPClientContext) Debug() bool {

View file

@ -201,6 +201,7 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
if err != nil {
return nil, err
}
setStartDirectory(user.Filters.StartDirectory, cc)
connection.Log(logger.LevelInfo, "User %#v logged in with %#v from ip %#v", user.Username, loginMethod, ipAddr)
dataprovider.UpdateLastLogin(&user)
return connection, nil
@ -246,6 +247,7 @@ func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsCo
if err != nil {
return nil, err
}
setStartDirectory(dbUser.Filters.StartDirectory, cc)
connection.Log(logger.LevelInfo, "User id: %d, logged in with FTP using a TLS certificate, username: %#v, home_dir: %#v remote addr: %#v",
dbUser.ID, dbUser.Username, dbUser.HomeDir, ipAddr)
dataprovider.UpdateLastLogin(&dbUser)
@ -367,6 +369,13 @@ func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext
return connection, nil
}
func setStartDirectory(startDirectory string, cc ftpserver.ClientContext) {
if startDirectory == "" {
return
}
cc.SetPath(startDirectory)
}
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metric.AddLoginAttempt(loginMethod)
if err != nil && err != common.ErrInternalFailure {

18
go.mod
View file

@ -8,11 +8,11 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go v1.43.7
github.com/aws/aws-sdk-go v1.43.10
github.com/cockroachdb/cockroach-go/v2 v2.2.8
github.com/coreos/go-oidc/v3 v3.1.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.17.1-0.20220212161409-5157f18d716f
github.com/fclairamb/ftpserverlib v0.17.1-0.20220302132530-f366fc1586cb
github.com/fclairamb/go-log v0.2.0
github.com/go-chi/chi/v5 v5.0.8-0.20220103230436-7dbe9a0bd10f
github.com/go-chi/jwtauth/v5 v5.0.2
@ -27,22 +27,22 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.14.4
github.com/lestrrat-go/jwx v1.2.19
github.com/lestrrat-go/jwx v1.2.20
github.com/lib/pq v1.10.4
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.12
github.com/mhale/smtpd v0.8.0
github.com/minio/sio v0.3.0
github.com/otiai10/copy v1.7.0
github.com/pires/go-proxyproto v0.6.1
github.com/pkg/sftp v1.13.5-0.20220119192800-7d25d533c9a3
github.com/pires/go-proxyproto v0.6.2
github.com/pkg/sftp v1.13.5-0.20220303113417-dcfc1d5e4162
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.12.1
github.com/rs/cors v1.8.2
github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.26.2-0.20220227173336-263b0bde3672
github.com/sftpgo/sdk v0.1.1-0.20220228183957-d7251ba29961
github.com/shirou/gopsutil/v3 v3.22.1
github.com/sftpgo/sdk v0.1.1-0.20220303113613-e279f0a57712
github.com/shirou/gopsutil/v3 v3.22.2
github.com/spf13/afero v1.8.1
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.1
@ -67,7 +67,7 @@ require (
require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.5.0 // indirect
cloud.google.com/go/iam v0.2.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v0.9.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
@ -130,7 +130,7 @@ require (
golang.org/x/tools v0.1.9 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220228155957-1da8797a5878 // indirect
google.golang.org/genproto v0.0.0-20220302033224-9aa15565e42a // indirect
google.golang.org/grpc v1.44.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect

36
go.sum
View file

@ -55,8 +55,8 @@ cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1
cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw=
cloud.google.com/go/iam v0.2.0 h1:Ouq6qif4mZdXkb3SiFMpxvu0JQJB1Yid9TsZ23N6hg8=
cloud.google.com/go/iam v0.2.0/go.mod h1:BCK88+tmjAwnZYfOSizmKCTSFjJHCa18t3DpdGEY13Y=
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/kms v0.1.0 h1:VXAb5OzejDcyhFzIDeZ5n5AUdlsFnCyexuascIwWMj0=
cloud.google.com/go/kms v0.1.0/go.mod h1:8Qp8PCAypHg4FdmlyW1QRAv09BGQ9Uzh7JnmIZxPk+c=
cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE=
@ -144,8 +144,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.43.7 h1:Gbs53KxXJWbO3txoVkevf56bhdDFqRisl7MQQ6581vc=
github.com/aws/aws-sdk-go v1.43.7/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go v1.43.10 h1:lFX6gzTBltYBnlJBjd2DWRCmqn2CbTcs6PW99/Dme7k=
github.com/aws/aws-sdk-go v1.43.10/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
@ -245,8 +245,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fclairamb/ftpserverlib v0.17.1-0.20220212161409-5157f18d716f h1:75ugogj/lKTVyDHTm0c5zgA16Fpfo/xiNpo8D/zn+TA=
github.com/fclairamb/ftpserverlib v0.17.1-0.20220212161409-5157f18d716f/go.mod h1:1y0ShfZWIRcgU0mVJaCjEYIu2+g37cRHgDIT8jemeO0=
github.com/fclairamb/ftpserverlib v0.17.1-0.20220302132530-f366fc1586cb h1:2gBRfMEhjADP8KN88nmq3Py8+vsXhdXyocfETy8gmaI=
github.com/fclairamb/ftpserverlib v0.17.1-0.20220302132530-f366fc1586cb/go.mod h1:RpiJGed4zOypZ2uy2xnujfTQvveToG6VQRhap7ke4x4=
github.com/fclairamb/go-log v0.2.0 h1:HzeOyomBVd0tEVLdIK0bBZr0j3xNip+zE1OqC1i5kbM=
github.com/fclairamb/go-log v0.2.0/go.mod h1:sd5oPNsxdVKRgWI8fVke99GXONszE3bsni2JxQMz8RU=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -546,8 +546,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
github.com/lestrrat-go/jwx v1.2.19 h1:qxxLmAXNwZpTTvjc4PH21nT7I4wPK6lVv3lVNcZPnUk=
github.com/lestrrat-go/jwx v1.2.19/go.mod h1:bWTBO7IHHVMtNunM8so9MT8wD+euEY1PzGEyCnuI2qM=
github.com/lestrrat-go/jwx v1.2.20 h1:ckMNlG0MqCcVp7LnD5FN2+459ndm7SW3vryE79Dz9nk=
github.com/lestrrat-go/jwx v1.2.20/go.mod h1:tLE1XszaFgd7zaS5wHe4NxA+XVhu7xgdRvDpNyi3kNM=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -636,16 +636,16 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pires/go-proxyproto v0.6.1 h1:EBupykFmo22SDjv4fQVQd2J9NOoLPmyZA/15ldOGkPw=
github.com/pires/go-proxyproto v0.6.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.0/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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/sftp v1.13.5-0.20220119192800-7d25d533c9a3 h1:gyvzmVdk4vso+w4gt8x2YtMdbAGSyX5KnekiEsbDLvQ=
github.com/pkg/sftp v1.13.5-0.20220119192800-7d25d533c9a3/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pkg/sftp v1.13.5-0.20220303113417-dcfc1d5e4162 h1:uJSlAAzEUQq5tpfK+SWIIx/3UJ4EpjAYuMqZpKYrmw4=
github.com/pkg/sftp v1.13.5-0.20220303113417-dcfc1d5e4162/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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@ -700,10 +700,10 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
github.com/sftpgo/sdk v0.1.1-0.20220228183957-d7251ba29961 h1:XpSoX58U9KR5qbexs3VUBZvgcRogjgbALWzQO4TIZKo=
github.com/sftpgo/sdk v0.1.1-0.20220228183957-d7251ba29961/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
github.com/shirou/gopsutil/v3 v3.22.1 h1:33y31Q8J32+KstqPfscvFwBlNJ6xLaBy4xqBXzlYV5w=
github.com/shirou/gopsutil/v3 v3.22.1/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
github.com/sftpgo/sdk v0.1.1-0.20220303113613-e279f0a57712 h1:+Rgx0SgsDnFSI5JBwL4mcCH2lkx3yKhLWcQnf0s2JKE=
github.com/sftpgo/sdk v0.1.1-0.20220303113613-e279f0a57712/go.mod h1:zqCRMcwS28IViwekJHNkFu4GqSfyVmOQTlh8h3icAXE=
github.com/shirou/gopsutil/v3 v3.22.2 h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks=
github.com/shirou/gopsutil/v3 v3.22.2/go.mod h1:WapW1AOOPlHyXr+yOyw3uYx36enocrtSoSBy0L5vUHY=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -1190,8 +1190,8 @@ google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2
google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220228155957-1da8797a5878 h1:gERY0VtsF9UyyyCsPSjRk9/RWlcKSa/Gw/aenR/5z48=
google.golang.org/genproto v0.0.0-20220228155957-1da8797a5878/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220302033224-9aa15565e42a h1:uqouglH745GoGeZ1YFZbPBiu961tgi/9Qm5jaorajjQ=
google.golang.org/genproto v0.0.0-20220302033224-9aa15565e42a/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

View file

@ -55,7 +55,7 @@ func readUserFolder(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
contents, err := connection.ReadDir(name)
if err != nil {
sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err))
@ -73,7 +73,7 @@ func createUserDir(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
if getBoolQueryParam(r, "mkdir_parents") {
if err = connection.CheckParentDirs(path.Dir(name)); err != nil {
sendAPIResponse(w, r, err, "Error checking parent directories", getMappedStatusCode(err))
@ -97,8 +97,8 @@ func renameUserDir(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
oldName := util.CleanPath(r.URL.Query().Get("path"))
newName := util.CleanPath(r.URL.Query().Get("target"))
oldName := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
newName := connection.User.GetCleanedPath(r.URL.Query().Get("target"))
err = connection.Rename(oldName, newName)
if err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to rename directory %#v to %#v", oldName, newName),
@ -117,7 +117,7 @@ func deleteUserDir(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
err = connection.RemoveDir(name)
if err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to delete directory %#v", name), getMappedStatusCode(err))
@ -135,7 +135,7 @@ func getUserFile(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
if name == "/" {
sendAPIResponse(w, r, nil, "Please set the path to a valid file", http.StatusBadRequest)
return
@ -186,7 +186,7 @@ func setFileDirMetadata(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
attrs := common.StatAttributes{
Flags: common.StatAttrTimes,
Atime: util.GetTimeFromMsecSinceEpoch(mTime),
@ -217,7 +217,7 @@ func uploadUserFile(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
filePath := util.CleanPath(r.URL.Query().Get("path"))
filePath := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
if getBoolQueryParam(r, "mkdir_parents") {
if err = connection.CheckParentDirs(path.Dir(filePath)); err != nil {
sendAPIResponse(w, r, err, "Error checking parent directories", getMappedStatusCode(err))
@ -279,7 +279,7 @@ func uploadUserFiles(w http.ResponseWriter, r *http.Request) {
connection.RemoveTransfer(t)
defer r.MultipartForm.RemoveAll() //nolint:errcheck
parentDir := util.CleanPath(r.URL.Query().Get("path"))
parentDir := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
files := r.MultipartForm.File["filenames"]
if len(files) == 0 {
sendAPIResponse(w, r, nil, "No files uploaded!", http.StatusBadRequest)
@ -339,8 +339,8 @@ func renameUserFile(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
oldName := util.CleanPath(r.URL.Query().Get("path"))
newName := util.CleanPath(r.URL.Query().Get("target"))
oldName := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
newName := connection.User.GetCleanedPath(r.URL.Query().Get("target"))
err = connection.Rename(oldName, newName)
if err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to rename file %#v to %#v", oldName, newName),
@ -359,7 +359,7 @@ func deleteUserFile(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
fs, p, err := connection.GetFsAndResolvedPath(name)
if err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to delete file %#v", name), getMappedStatusCode(err))

View file

@ -217,7 +217,7 @@ func renderCompressedFiles(w http.ResponseWriter, conn *Connection, baseDir stri
wr := zip.NewWriter(w)
for _, file := range files {
fullPath := path.Join(baseDir, file)
fullPath := util.CleanPath(path.Join(baseDir, file))
if err := addZipEntry(wr, conn, fullPath, baseDir); err != nil {
if share != nil {
dataprovider.UpdateShareLastUse(share, -1) //nolint:errcheck
@ -252,7 +252,7 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er
return err
}
for _, info := range contents {
fullPath := path.Join(entryPath, info.Name())
fullPath := util.CleanPath(path.Join(entryPath, info.Name()))
if err := addZipEntry(wr, conn, fullPath, baseDir); err != nil {
return err
}

View file

@ -62,7 +62,6 @@ func (c *Connection) GetCommand() string {
func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {
c.UpdateLastActivity()
name = util.CleanPath(name)
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {
return nil, c.GetPermissionDeniedError()
}
@ -78,7 +77,6 @@ func (c *Connection) Stat(name string, mode int) (os.FileInfo, error) {
func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
c.UpdateLastActivity()
name = util.CleanPath(name)
return c.ListDir(name)
}
@ -91,7 +89,6 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io
return nil, c.GetReadQuotaExceededError()
}
name = util.CleanPath(name)
if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) {
return nil, c.GetPermissionDeniedError()
}

View file

@ -11314,6 +11314,146 @@ func TestWebFilesAPI(t *testing.T) {
checkResponseCode(t, http.StatusNotFound, rr)
}
func TestStartDirectory(t *testing.T) {
u := getTestUser()
u.Filters.StartDirectory = "/start/dir"
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
filename := "file1.txt"
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part1, err := writer.CreateFormFile("filenames", filename)
assert.NoError(t, err)
_, err = part1.Write([]byte("test content"))
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
reader := bytes.NewReader(body.Bytes())
req, err := http.NewRequest(http.MethodPost, userFilesPath, reader)
assert.NoError(t, err)
req.Header.Add("Content-Type", writer.FormDataContentType())
setBearerForReq(req, webAPIToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
// check we have 2 files in the defined start dir
req, err = http.NewRequest(http.MethodGet, userDirsPath, nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
var contents []map[string]interface{}
err = json.NewDecoder(rr.Body).Decode(&contents)
assert.NoError(t, err)
if assert.Len(t, contents, 1) {
assert.Equal(t, filename, contents[0]["name"].(string))
}
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=file2.txt",
bytes.NewBuffer([]byte("single upload content")))
assert.NoError(t, err)
req.Header.Add("Content-Type", writer.FormDataContentType())
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=testdir", nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path=testdir&target=testdir1", nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodPost, userDirsPath+"?path=%2Ftestdirroot", nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
req, err = http.NewRequest(http.MethodGet, userDirsPath+"?path="+url.QueryEscape(u.Filters.StartDirectory), nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
contents = nil
err = json.NewDecoder(rr.Body).Decode(&contents)
assert.NoError(t, err)
assert.Len(t, contents, 3)
req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path="+filename, nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path=%2F"+filename, nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path="+filename+"&target="+filename+"_rename", nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path=testdir1", nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, userDirsPath, nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
contents = nil
err = json.NewDecoder(rr.Body).Decode(&contents)
assert.NoError(t, err)
assert.Len(t, contents, 2)
req, err = http.NewRequest(http.MethodGet, webClientDirsPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
contents = nil
err = json.NewDecoder(rr.Body).Decode(&contents)
assert.NoError(t, err)
assert.Len(t, contents, 2)
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path="+filename+"_rename", nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, userDirsPath+"?path="+url.QueryEscape(u.Filters.StartDirectory), nil)
assert.NoError(t, err)
setBearerForReq(req, webAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
contents = nil
err = json.NewDecoder(rr.Body).Decode(&contents)
assert.NoError(t, err)
assert.Len(t, contents, 1)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestWebFilesTransferQuotaLimits(t *testing.T) {
u := getTestUser()
u.UploadDataTransfer = 1
@ -13947,6 +14087,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("disable_fs_checks", "checked")
form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("start_directory", "start/dir")
b, contentType, _ := getMultipartFormData(form, "", "")
// test invalid url escape
req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b)
@ -14228,6 +14369,7 @@ func TestWebUserAddMock(t *testing.T) {
assert.True(t, newUser.Filters.DisableFsChecks)
assert.False(t, newUser.Filters.AllowAPIKeyAuth)
assert.Equal(t, user.Email, newUser.Email)
assert.Equal(t, "/start/dir", newUser.Filters.StartDirectory)
assert.True(t, util.IsStringInSlice(testPubKey, newUser.PublicKeys))
if val, ok := newUser.Permissions["/subdir"]; ok {
assert.True(t, util.IsStringInSlice(dataprovider.PermListItems, val))

View file

@ -948,6 +948,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
filters.StartDirectory = r.Form.Get("start_directory")
return filters, err
}

View file

@ -598,11 +598,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := "/"
if _, ok := r.URL.Query()["path"]; ok {
name = util.CleanPath(r.URL.Query().Get("path"))
}
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
files := r.URL.Query().Get("files")
var filesList []string
err = json.Unmarshal([]byte(files), &filesList)
@ -742,11 +738,7 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http.
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := "/"
if _, ok := r.URL.Query()["path"]; ok {
name = util.CleanPath(r.URL.Query().Get("path"))
}
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
contents, err := connection.ReadDir(name)
if err != nil {
sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err))
@ -820,10 +812,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := "/"
if _, ok := r.URL.Query()["path"]; ok {
name = util.CleanPath(r.URL.Query().Get("path"))
}
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
var info os.FileInfo
if name == "/" {
info = vfs.NewFileInfo(name, true, 0, time.Now(), false)
@ -880,7 +869,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
common.Connections.Add(connection)
defer common.Connections.Remove(connection.GetID())
name := util.CleanPath(r.URL.Query().Get("path"))
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
info, err := connection.Stat(name, 0)
if err != nil {
renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %#v", name), "",

View file

@ -1484,6 +1484,10 @@ func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovid
return errors.New("web client options contents mismatch")
}
}
return compareUserFiltersEqualFields(expected, actual)
}
func compareUserFiltersEqualFields(expected *dataprovider.User, actual *dataprovider.User) error {
if expected.Filters.Hooks.ExternalAuthDisabled != actual.Filters.Hooks.ExternalAuthDisabled {
return errors.New("external_auth_disabled hook mismatch")
}
@ -1496,6 +1500,9 @@ func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovid
if expected.Filters.DisableFsChecks != actual.Filters.DisableFsChecks {
return errors.New("disable_fs_checks mismatch")
}
if expected.Filters.StartDirectory != actual.Filters.StartDirectory {
return errors.New("start_directory mismatch")
}
return nil
}

View file

@ -211,7 +211,7 @@ paths:
parameters:
- in: query
name: path
description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the root folder is assumed
description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the user's start directory is assumed. If relative, the user's start directory is used as the base
schema:
type: string
responses:
@ -3632,7 +3632,7 @@ paths:
parameters:
- in: query
name: path
description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the root folder is assumed
description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the user's start directory is assumed. If relative, the user's start directory is used as the base
schema:
type: string
responses:
@ -3664,7 +3664,7 @@ paths:
parameters:
- in: query
name: path
description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the root folder is assumed
description: Path to the folder to read. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir". If empty or missing the user's start directory is assumed. If relative, the user's start directory is used as the base
schema:
type: string
responses:
@ -4679,6 +4679,9 @@ components:
external_auth_cache_time:
type: integer
description: 'Defines the cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache'
start_directory:
type: string
description: 'Specifies an alternate starting directory. If not set, the default is "/". This option is supported for SFTP/SCP, FTP and HTTP (WebClient/REST API) protocols. Relative paths will use this directory as base.'
description: Additional user options
Secret:
type: object

View file

@ -405,6 +405,7 @@ func TestSSHCommandPath(t *testing.T) {
}
connection := &Connection{
channel: &mockSSHChannel,
BaseConnection: common.NewBaseConnection("", common.ProtocolSSH, "", "", dataprovider.User{}),
}
sshCommand := sshCommand{
command: "test",

View file

@ -553,7 +553,8 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
defer common.Connections.Remove(connection.GetID())
// Create the server instance for the channel using the handler we created above.
server := sftp.NewRequestServer(channel, c.createHandlers(connection), sftp.WithRSAllocator())
server := sftp.NewRequestServer(channel, c.createHandlers(connection), sftp.WithRSAllocator(),
sftp.WithStartDirectory(connection.User.Filters.StartDirectory))
defer server.Close()
if err := server.Serve(); err == io.EOF {

View file

@ -538,6 +538,68 @@ func TestBasicSFTPFsHandling(t *testing.T) {
assert.NoError(t, err)
}
func TestStartDirectory(t *testing.T) {
usePubKey := false
startDir := "/st@ rt/dir"
u := getTestUser(usePubKey)
u.Filters.StartDirectory = startDir
user, _, 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()
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)
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)
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)
err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestFolderPrefix(t *testing.T) {
usePubKey := true
u := getTestUser(usePubKey)
@ -9184,6 +9246,39 @@ func TestSCPRecursive(t *testing.T) {
assert.NoError(t, err)
}
func TestSCPStartDirectory(t *testing.T) {
usePubKey := true
startDir := "/sta rt/dir"
u := getTestUser(usePubKey)
u.Filters.StartDirectory = startDir
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
testFileSize := int64(131072)
testFilePath := filepath.Join(homeBasePath, testFileName)
localPath := filepath.Join(homeBasePath, "scp_download.dat")
remoteUpPath := fmt.Sprintf("%v@127.0.0.1:", user.Username)
remoteDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, testFileName)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = scpUpload(testFilePath, remoteUpPath, false, false)
assert.NoError(t, err)
err = scpDownload(localPath, remoteDownPath, false, false)
assert.NoError(t, err)
// check that the file is in the start directory
_, err = os.Stat(filepath.Join(user.HomeDir, startDir, testFileName))
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(localPath)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestSCPPatternsFilter(t *testing.T) {
if len(scpPath) == 0 {
t.Skip("scp command not found, unable to execute this test")

View file

@ -531,7 +531,7 @@ func (c *sshCommand) getDestPath() string {
if len(c.args) == 0 {
return ""
}
return cleanCommandPath(c.args[len(c.args)-1])
return c.cleanCommandPath(c.args[len(c.args)-1])
}
// for the supported commands, the destination path, if any, is the second-last argument
@ -539,13 +539,13 @@ func (c *sshCommand) getSourcePath() string {
if len(c.args) < 2 {
return ""
}
return cleanCommandPath(c.args[len(c.args)-2])
return c.cleanCommandPath(c.args[len(c.args)-2])
}
func cleanCommandPath(name string) string {
func (c *sshCommand) cleanCommandPath(name string) string {
name = strings.Trim(name, "'")
name = strings.Trim(name, "\"")
result := util.CleanPath(name)
result := c.connection.User.GetCleanedPath(name)
if strings.HasSuffix(name, "/") && !strings.HasSuffix(result, "/") {
result += "/"
}

View file

@ -821,6 +821,17 @@
<div id="collapseAdvanced" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionUser">
<div class="card-body">
<div class="form-group row">
<label for="idStartDirectory" class="col-sm-2 col-form-label">Start directory</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idStartDirectory" name="start_directory" placeholder=""
value="{{.User.Filters.StartDirectory}}" aria-describedby="startDirHelpBlock">
<small id="startDirHelpBlock" class="form-text text-muted">
Alternate start directory to use instead of "/". Supported for SFTP/FTP/HTTP
</small>
</div>
</div>
<div class="form-group row">
<label for="idTLSUsername" class="col-sm-2 col-form-label">TLS username</label>
<div class="col-sm-10">

View file

@ -83,7 +83,7 @@ type Fs interface {
IsUploadResumeSupported() bool
IsAtomicUploadSupported() bool
CheckRootPath(username string, uid int, gid int) bool
ResolvePath(sftpPath string) (string, error)
ResolvePath(virtualPath string) (string, error)
IsNotExist(err error) bool
IsPermission(err error) bool
IsNotSupported(err error) bool