add support for a start directory
Fixes #705 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
4519bffa39
commit
5c2fd8d52a
28 changed files with 478 additions and 94 deletions
20
.github/workflows/development.yml
vendored
20
.github/workflows/development.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -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
|
||||
|
|
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
|
@ -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: |
|
||||
|
|
|
@ -29,6 +29,7 @@ var (
|
|||
portableAdvertiseCredentials bool
|
||||
portableUsername string
|
||||
portablePassword string
|
||||
portableStartDir string
|
||||
portableLogFile string
|
||||
portableLogVerbose bool
|
||||
portableLogUTCTime bool
|
||||
|
@ -163,7 +164,8 @@ Please take a look at the usage below to customize the serving parameters`,
|
|||
},
|
||||
Filters: dataprovider.UserFilters{
|
||||
BaseUserFilters: sdk.BaseUserFilters{
|
||||
FilePatterns: parsePatternsFilesFilters(),
|
||||
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,
|
||||
|
|
|
@ -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/"))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
18
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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), "",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -404,7 +404,8 @@ func TestSSHCommandPath(t *testing.T) {
|
|||
ReadError: nil,
|
||||
}
|
||||
connection := &Connection{
|
||||
channel: &mockSSHChannel,
|
||||
channel: &mockSSHChannel,
|
||||
BaseConnection: common.NewBaseConnection("", common.ProtocolSSH, "", "", dataprovider.User{}),
|
||||
}
|
||||
sshCommand := sshCommand{
|
||||
command: "test",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 += "/"
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue