add httpfs

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-06-11 10:41:34 +02:00
parent 3170991aa8
commit 7ab30099dd
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
45 changed files with 3008 additions and 344 deletions

View file

@ -271,6 +271,10 @@ Each user can be mapped to another SFTP server account or a subfolder of it. Mor
Data at-rest encryption is supported via the [cryptfs backend](./docs/dare.md).
### HTTP/S backend
HTTP/S backend allows you to write your own custom storage backend by implementing a REST API. More information can be found [here](./docs/httpfs.md).
### Other Storage backends
Adding new storage backends is quite easy:

View file

@ -136,6 +136,8 @@ func newActionNotification(
}
case sdk.SFTPFilesystemProvider:
endpoint = fsConfig.SFTPConfig.Endpoint
case sdk.HTTPFilesystemProvider:
endpoint = fsConfig.HTTPConfig.Endpoint
}
if err == ErrQuotaExceeded {

View file

@ -49,6 +49,11 @@ func TestNewActionNotification(t *testing.T) {
Endpoint: "sftpendpoint",
},
}
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: "httpendpoint",
},
}
sessionID := xid.New().String()
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
123, 0, errors.New("fake error"))
@ -71,6 +76,12 @@ func TestNewActionNotification(t *testing.T) {
assert.Equal(t, 0, len(a.Endpoint))
assert.Equal(t, 3, a.Status)
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID,
123, 0, nil)
assert.Equal(t, "httpendpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
123, 0, nil)

View file

@ -104,21 +104,20 @@ func init() {
// errors definitions
var (
ErrPermissionDenied = errors.New("permission denied")
ErrNotExist = errors.New("no such file or directory")
ErrOpUnsupported = errors.New("operation unsupported")
ErrGenericFailure = errors.New("failure")
ErrQuotaExceeded = errors.New("denying write due to space limit")
ErrReadQuotaExceeded = errors.New("denying read due to quota limit")
ErrSkipPermissionsCheck = errors.New("permission check skipped")
ErrConnectionDenied = errors.New("you are not allowed to connect")
ErrNoBinding = errors.New("no binding configured")
ErrCrtRevoked = errors.New("your certificate has been revoked")
ErrNoCredentials = errors.New("no credential provided")
ErrInternalFailure = errors.New("internal failure")
ErrTransferAborted = errors.New("transfer aborted")
errNoTransfer = errors.New("requested transfer not found")
errTransferMismatch = errors.New("transfer mismatch")
ErrPermissionDenied = errors.New("permission denied")
ErrNotExist = errors.New("no such file or directory")
ErrOpUnsupported = errors.New("operation unsupported")
ErrGenericFailure = errors.New("failure")
ErrQuotaExceeded = errors.New("denying write due to space limit")
ErrReadQuotaExceeded = errors.New("denying read due to quota limit")
ErrConnectionDenied = errors.New("you are not allowed to connect")
ErrNoBinding = errors.New("no binding configured")
ErrCrtRevoked = errors.New("your certificate has been revoked")
ErrNoCredentials = errors.New("no credential provided")
ErrInternalFailure = errors.New("internal failure")
ErrTransferAborted = errors.New("transfer aborted")
errNoTransfer = errors.New("requested transfer not found")
errTransferMismatch = errors.New("transfer mismatch")
)
var (

View file

@ -752,7 +752,7 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
initialSize = info.Size()
err = fs.Truncate(fsPath, size)
}
if err == nil && vfs.IsLocalOrSFTPFs(fs) {
if err == nil && vfs.HasTruncateSupport(fs) {
sizeDiff := initialSize - size
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
if err == nil {
@ -768,22 +768,13 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
}
func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, sourcePath, targetPath string) error {
err := fsSrc.Walk(sourcePath, func(walkedPath string, info os.FileInfo, err error) error {
return fsSrc.Walk(sourcePath, func(walkedPath string, info os.FileInfo, err error) error {
if err != nil {
return c.GetFsError(fsSrc, err)
}
dstPath := strings.Replace(walkedPath, sourcePath, targetPath, 1)
virtualSrcPath := fsSrc.GetRelativePath(walkedPath)
virtualDstPath := fsDst.GetRelativePath(dstPath)
// walk scans the directory tree in order, checking the parent directory permissions we are sure that all contents
// inside the parent path was checked. If the current dir has no subdirs with defined permissions inside it
// and it has all the possible permissions we can stop scanning
if !c.User.HasPermissionsInside(path.Dir(virtualSrcPath)) && !c.User.HasPermissionsInside(path.Dir(virtualDstPath)) {
if c.User.HasPermsRenameAll(path.Dir(virtualSrcPath)) &&
c.User.HasPermsRenameAll(path.Dir(virtualDstPath)) {
return ErrSkipPermissionsCheck
}
}
if !c.isRenamePermitted(fsSrc, fsDst, walkedPath, dstPath, virtualSrcPath, virtualDstPath, info) {
c.Log(logger.LevelInfo, "rename %#v -> %#v is not allowed, virtual destination path: %#v",
walkedPath, dstPath, virtualDstPath)
@ -791,10 +782,6 @@ func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs,
}
return nil
})
if err == ErrSkipPermissionsCheck {
err = nil
}
return err
}
func (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath string, fi os.FileInfo) bool {

View file

@ -153,6 +153,8 @@ func (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) {
}
forbiddenSelfUsers = append(forbiddenSelfUsers, u.Username)
return vfs.NewSFTPFs(connectionID, "", u.GetHomeDir(), forbiddenSelfUsers, u.FsConfig.SFTPConfig)
case sdk.HTTPFilesystemProvider:
return vfs.NewHTTPFs(connectionID, u.GetHomeDir(), "", u.FsConfig.HTTPConfig)
default:
return vfs.NewOsFs(connectionID, u.GetHomeDir(), ""), nil
}
@ -1366,6 +1368,8 @@ func (u *User) GetStorageDescrition() string {
return fmt.Sprintf("Encrypted: %v", u.GetHomeDir())
case sdk.SFTPFilesystemProvider:
return fmt.Sprintf("SFTP: %v", u.FsConfig.SFTPConfig.Endpoint)
case sdk.HTTPFilesystemProvider:
return fmt.Sprintf("HTTP: %v", u.FsConfig.HTTPConfig.Endpoint)
default:
return ""
}
@ -1595,6 +1599,8 @@ func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem) vfs.Filesyst
case sdk.SFTPFilesystemProvider:
fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username)
fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix)
case sdk.HTTPFilesystemProvider:
fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username)
}
return fsConfig
}

View file

@ -4,11 +4,11 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
## Supported tags and respective Dockerfile links
- [v2.3.0, v2.3, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile)
- [v2.3.0-alpine, v2.3-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile.alpine)
- [v2.3.0-slim, v2.3-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile)
- [v2.3.0-alpine-slim, v2.3-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile.alpine)
- [v2.3.0-distroless-slim, v2.3-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile.distroless)
- [v2.3.1, v2.3, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile)
- [v2.3.1-alpine, v2.3-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile.alpine)
- [v2.3.1-slim, v2.3-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile)
- [v2.3.1-alpine-slim, v2.3-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile.alpine)
- [v2.3.1-distroless-slim, v2.3-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile.distroless)
- [edge](../Dockerfile)
- [edge-alpine](../Dockerfile.alpine)
- [edge-slim](../Dockerfile)

View file

@ -19,7 +19,7 @@ The following settings are inherited from the primary group:
The following settings are inherited from the primary and secondary groups:
- virtual folders, file patterns, permissions: they are added to the user configuration if the user does not already have a setting for the configured path. The `/` path is ignored for secondary groups. The `%username%` placeholder is replaced with the username within the virtual path, the defined "prefix", for any vfs, and the "username" for the SFTP filesystem config
- virtual folders, file patterns, permissions: they are added to the user configuration if the user does not already have a setting for the configured path. The `/` path is ignored for secondary groups. The `%username%` placeholder is replaced with the username within the virtual path, the defined "prefix", for any vfs, and the "username" for the SFTP and HTTP filesystem config
- per-source bandwidth limits
- per-source data transfer limits
- allowed/denied IPs

16
docs/httpfs.md Normal file
View file

@ -0,0 +1,16 @@
# HTTP/S storage backend
SFTPGo can use custom storage backend implementations compliant with the REST API documented [here](./../openapi/httpfs.yaml).
The only required parameter is the HTTP/S endpoint that SFTPGo must use to make API calls.
If you define `http://127.0.0.1:9999/api/v1` as endpoint, SFTPGo will add the API path, for example for the `stat` API it will invoke `http://127.0.0.1:9999/api/v1/stat/{name}`.
You can set a `username` and/or a `password` to instruct SFTPGo to use the basic authentication, or you can set an API key to instruct SFTPGo to add it to each API call in the `X-API-KEY` HTTP header.
Here is a mapping between HTTP response codes and protocol errors:
- `401`, `403` mean permission denied error
- `404`, means not found error
- `501`, means not supported error
- `200`, `201`, mean no error
- any other response code means a generic error

View file

@ -1,6 +1,6 @@
# SFTPGo repositories
These repositories are available through Oregon State University's free mirror service. Special thanks to Lance Albertson, Director of the Oregon State University Open Source Lab, who helped me with the initial setup.
These repositories are available through Oregon State University's free mirroring service. Special thanks to Lance Albertson, Director of the Oregon State University Open Source Lab, who helped me with the initial setup.
## APT repo

View file

@ -226,10 +226,12 @@ G6p7xS+JswJrzX4885bZJ9Oi1AR2yM3sC9l0O7I4lDbNPmWIXBLeEhGMmcPKv/Kc
w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p
xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
-----END RSA PRIVATE KEY-----`
testFileName = "test_file_ftp.dat"
testDLFileName = "test_download_ftp.dat"
tlsClient1Username = "client1"
tlsClient2Username = "client2"
testFileName = "test_file_ftp.dat"
testDLFileName = "test_download_ftp.dat"
tlsClient1Username = "client1"
tlsClient2Username = "client2"
httpFsPort = 23456
defaultHTTPFsUsername = "httpfs_ftp_user"
)
var (
@ -414,6 +416,7 @@ func TestMain(m *testing.M) {
waitTCPListening(ftpdConf.Bindings[0].GetAddress())
waitNoConnections()
startHTTPFs()
exitCode := m.Run()
os.Remove(logFilePath)
@ -595,6 +598,50 @@ func TestBasicFTPHandling(t *testing.T) {
50*time.Millisecond)
}
func TestHTTPFs(t *testing.T) {
u := getTestUserWithHTTPFs()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client, err := getFTPClient(user, true, nil)
if assert.NoError(t, err) {
err = checkBasicFTP(client)
assert.NoError(t, err)
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)
// test a download resume
data := []byte("test data")
err = os.WriteFile(testFilePath, data, os.ModePerm)
assert.NoError(t, err)
err = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)
assert.NoError(t, err)
err = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)-5), client, 5)
assert.NoError(t, err)
readed, err := os.ReadFile(localDownloadPath)
assert.NoError(t, err)
assert.Equal(t, []byte("data"), readed, "readed data mismatch: %q", string(readed))
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(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 1*time.Second, 50*time.Millisecond)
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
50*time.Millisecond)
}
func TestListDirWithWildcards(t *testing.T) {
localUser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
@ -3422,6 +3469,18 @@ func getTestSFTPUser() dataprovider.User {
return u
}
func getTestUserWithHTTPFs() dataprovider.User {
u := getTestUser()
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
Username: defaultHTTPFsUsername,
},
}
return u
}
func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse bool, username string) []byte {
extAuthContent := []byte("#!/bin/sh\n\n")
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
@ -3511,3 +3570,13 @@ func generateTOTPPasscode(secret string, algo otp.Algorithm) (string, error) {
Algorithm: algo,
})
}
func startHTTPFs() {
go func() {
if err := httpdtest.StartTestHTTPFs(httpFsPort); err != nil {
logger.ErrorToConsole("could not start HTTPfs test server: %v", err)
os.Exit(1)
}
}()
waitTCPListening(fmt.Sprintf(":%d", httpFsPort))
}

View file

@ -462,7 +462,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
file.Seek(initialSize, io.SeekStart) //nolint:errcheck // for sftp seek simply set the offset
}
} else {
if vfs.IsLocalOrSFTPFs(fs) {
if vfs.HasTruncateSupport(fs) {
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck

72
go.mod
View file

@ -8,16 +8,16 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
github.com/aws/aws-sdk-go-v2 v1.16.4
github.com/aws/aws-sdk-go-v2/config v1.15.9
github.com/aws/aws-sdk-go-v2/credentials v1.12.4
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.9
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6
github.com/cockroachdb/cockroach-go/v2 v2.2.11
github.com/aws/aws-sdk-go-v2 v1.16.5
github.com/aws/aws-sdk-go-v2/config v1.15.10
github.com/aws/aws-sdk-go-v2/credentials v1.12.5
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.15
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.11
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.10
github.com/aws/aws-sdk-go-v2/service/sts v1.16.7
github.com/cockroachdb/cockroach-go/v2 v2.2.12
github.com/coreos/go-oidc/v3 v3.2.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e
@ -31,7 +31,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.3.0
github.com/grandcat/zeroconf v1.0.0
github.com/hashicorp/go-hclog v1.2.0
github.com/hashicorp/go-hclog v1.2.1
github.com/hashicorp/go-plugin v1.4.4
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
@ -50,13 +50,13 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.8.2
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.26.2-0.20220505171737-a4ec5e4cdd4b
github.com/sftpgo/sdk v0.1.1
github.com/rs/zerolog v1.27.0
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
github.com/shirou/gopsutil/v3 v3.22.5
github.com/spf13/afero v1.8.2
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.1
github.com/stretchr/testify v1.7.2
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
github.com/unrolled/secure v1.10.0
github.com/wagslane/go-password-validator v0.3.0
@ -66,11 +66,11 @@ require (
go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.25.0
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
golang.org/x/net v0.0.0-20220531201128-c960675eff93
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
golang.org/x/time v0.0.0-20220411224347-583f2d630306
google.golang.org/api v0.82.0
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
google.golang.org/api v0.83.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
@ -79,17 +79,17 @@ require (
cloud.google.com/go/compute v1.6.1 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 // indirect
github.com/aws/smithy-go v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.8 // indirect
github.com/aws/smithy-go v1.11.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
@ -132,7 +132,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
@ -149,12 +149,12 @@ require (
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
golang.org/x/tools v0.1.11 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
@ -166,5 +166,5 @@ require (
replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220527053356-5e1caf8ed0e1
golang.org/x/net => github.com/drakkan/net v0.0.0-20220603083515-6ce0d6be4d73
golang.org/x/net => github.com/drakkan/net v0.0.0-20220609171611-ca8008b74f1f
)

142
go.sum
View file

@ -136,64 +136,67 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
github.com/aws/aws-sdk-go-v2 v1.16.4 h1:swQTEQUyJF/UkEA94/Ga55miiKFoXmm/Zd67XHgmjSg=
github.com/aws/aws-sdk-go-v2 v1.16.4/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZMvr6MrSficMtjY2oS0WOORXTlxwU=
github.com/aws/aws-sdk-go-v2 v1.16.5 h1:Ah9h1TZD9E2S1LzHpViBO3Jz9FPL5+rmflmb8hXirtI=
github.com/aws/aws-sdk-go-v2 v1.16.5/go.mod h1:Wh7MEsmEApyL5hrWzpDkba4gwAPc5/piwLVLFnCxp48=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.2 h1:LFOGNUQxc/8BlhA4FD+JdYjJKQK6tsz9Xiuh+GUTKAQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.2/go.mod h1:u/38zebMi809w7YFnqY/07Tw/FSs6DGhPD95Xiig7XQ=
github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg=
github.com/aws/aws-sdk-go-v2/config v1.15.9 h1:TK5yNEnFDQ9iaO04gJS/3Y+eW8BioQiCUafW75/Wc3Q=
github.com/aws/aws-sdk-go-v2/config v1.15.9/go.mod h1:rv/l/TbZo67kp99v/3Kb0qV6Fm1KEtKyruEV2GvVfgs=
github.com/aws/aws-sdk-go-v2/config v1.15.10 h1:0HSMRNGlR0/WlGbeKC9DbBphBwRIK5H4cKUbgqNTKcA=
github.com/aws/aws-sdk-go-v2/config v1.15.10/go.mod h1:XL4DzwzWdwXBzKdwMdpLkMIaGEQCYRQyzA4UnJaUnNk=
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g=
github.com/aws/aws-sdk-go-v2/credentials v1.12.4 h1:xggwS+qxCukXRVXJBJWQJGyUsvuxGC8+J1kKzv2cxuw=
github.com/aws/aws-sdk-go-v2/credentials v1.12.4/go.mod h1:7g+GGSp7xtR823o1jedxKmqRZGqLdoHQfI4eFasKKxs=
github.com/aws/aws-sdk-go-v2/credentials v1.12.5 h1:WNNCUTWA0vyMy5t8LfS4iB7QshsW0DsHS/VdhyCGZWM=
github.com/aws/aws-sdk-go-v2/credentials v1.12.5/go.mod h1:DOcdLlkqUiNGyXnjWgspC3eIAdXhj8q0pO1LiSvrTI4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 h1:YPxclBeE07HsLQE8vtjC8T2emcTjM9nzqsnDi2fv5UM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5/go.mod h1:WAPnuhG5IQ/i6DETFl5NmX3kKqCzw7aau9NHAGcm4QE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6 h1:+NZzDh/RpcQTpo9xMFUgkseIam6PC+YJbdhbQp1NOXI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6/go.mod h1:ClLMcuQA/wcHPmOIfNzNI4Y1Q0oDbmEkbYhMFOzHDh8=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14 h1:qpJmFbypCfwPok5PGTSnQy1NKbv4Hn8xGsee9l4xOPE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14/go.mod h1:IOYB+xOZik8YgdTlnDSwbvKmCkikA3nVue8/Qnfzs0c=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.15 h1:WrTFSORSXKw+ZNV5CJnQjHgACSsteMyq2Oy9psCxtl4=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.15/go.mod h1:t/cWdEpu8thFU8Gv3SQnDiRq+g5heJPcHtrCbpUZR4E=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 h1:gsqHplNh1DaQunEKZISK56wlpbCg0yKxNVvGWCFuF1k=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11/go.mod h1:tmUB6jakq5DFNcXsXOA/ZQ7/C8VnSKYkx58OI7Fh79g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 h1:Zt7DDk5V7SyQULUUwIKzsROtVzp/kVvcz15uQx/Tkow=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12/go.mod h1:Afj/U8svX6sJ77Q+FPWMzabJ9QjbwP32YlopgKALUpg=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 h1:PLFj+M2PgIDHG//hw3T0O0KLI4itVtAjtxrZx4AHPLg=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5/go.mod h1:fV1AaS2gFc1tM0RCb015FJ0pvWVUfJZANzjwoO4YakM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 h1:eeXdGVtXEe+2Jc49+/vAzna3FAQnUD4AagAw8tzbmfc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6/go.mod h1:FwpAKI+FBPIELJIdmQzlLtRe8LQSOreMcM2wBsPMvvc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 h1:j0VqrjtgsY1Bx27tD0ysay36/K4kFMWRp9K3ieO9nLU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12/go.mod h1:00c7+ALdPh4YeEUPXJzyU0Yy01nPGOq2+9rUaz05z9g=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2 h1:1fs9WkbFcMawQjxEI0B5L0SqvBhJZebxWM6Z3x/qHWY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2/go.mod h1:0jDVeWUFPbI3sOfsXXAsIdiawXcn7VBLx/IlFVTRP64=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 h1:T4pFel53bkHjL2mMo+4DKE6r6AuoZnM0fg7k1/ratr4=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13 h1:L/l0WbIpIadRO7i44jZh1/XeXpNDX0sokFppb4ZnXUI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13/go.mod h1:hiM/y1XPp3DoEPhoVEYc/CZcS58dP6RKJRDFp99wdX0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.3 h1:m1vDVDoNK4tZAoWtcetHopEdIeUlrNNpdLZ7cwZke6s=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.3/go.mod h1:annFthsb7FiHQd5X9wKDNst9OJvVFY0l0LjQ8zQniJA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 h1:T/ywkX1ed+TsZVQccu/8rRJGxKZF/t0Ivgrb4MHTSeo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2/go.mod h1:RnloUnyZ4KN9JStGY1LuQ7Wzqh7V0f8FinmRdHYtuaA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6 h1:9mvDAsMiN+07wcfGM+hJ1J3dOKZ2YOpDiPZ6ufRJcgw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6/go.mod h1:Eus+Z2iBIEfhOvhSdMTcscNOMy6n3X9/BJV0Zgax98w=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.7 h1:DYUAx8lWAhIzFiD284oq6RUPKppKk3cyqv/hyUkbWuA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.7/go.mod h1:6tcs0yjwAW2Z9Yb3Z4X/2tm3u9jNox1dvXxVXTd73Zw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 h1:gRW1ZisKc93EWEORNJRvy/ZydF3o6xLSveJHdi1Oa0U=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5/go.mod h1:ZbkttHXaVn3bBo/wpJbQGiiIWR90eTBUVBrEHUEQlho=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6 h1:0ZxYAZ1cn7Swi/US55VKciCE6RhRHIwCKIWaMLdT6pg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6/go.mod h1:DxAPjquoEHf3rUHh1b9+47RAaXB8/7cB6jkzCt/GOEI=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5 h1:DyPYkrH4R2zn+Pdu6hM3VTuPsQYAE6x2WB24X85Sgw0=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5/go.mod h1:XtL92YWo0Yq80iN3AgYRERJqohg4TozrqRlxYhHGJ7g=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.6 h1:SSrqxZVhrO371eg/C8Fnj6kduzltKHj/mJl2swkTBGc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.6/go.mod h1:TzDyqDka0783D93yVirkcysbibVRxjX5HFJEWms4kKA=
github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.5 h1:CvRAsgxd1BN5l961+xXfS0mEhhyJTMxqdoWpZQIJZt4=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.5/go.mod h1:tnwCNkQvihXdRZ8Fyita7EJ0IeY46DcJWhgcWaquT+o=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.6 h1:QHv9AYaolQo8Tj+PIfIizQ/aD/EHrb7eOlNpeuEKyEU=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.6/go.mod h1:M8WgODCojJa7pJRL7vx2bS4NO+NjcRtlvGDr9ls/MAI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10 h1:GWdLZK0r1AK5sKb8rhB9bEXqXCK8WNuyv4TBAD6ZviQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10/go.mod h1:+O7qJxF8nLorAhuIVhYTHse6okjHJJm4EwhhzvpnkT0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.11 h1:Wt0512f6GfLiMd6a+NuOCC9r3/trmzHMTB697CBDUwg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.11/go.mod h1:VMTprbiZWqW44viXgPSQhWdeZ8JTAeJwhO7OXpC/Rsg=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.9 h1:a7+ZYQbKAziY5a7H8Ggwp/6HM9UKT6h9al+QHY+P6jI=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.9/go.mod h1:Jt1lSw1fYlQ60lqrZ9ViN2LMGizbWTWbkStm4rbuYuE=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.10 h1:quGsZJn6aaTtmplz+AwPSukYWuD6LFJiQJZmD8M+YPk=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.10/go.mod h1:pgtQihVJw8OxQCkC4BmJOuVWT52mBTaj8LcsF5Kr9iA=
github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw=
github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 h1:suAGD+RyiHWPPihZzY+jw4mCZlOFWgmdjb2AeTenz7c=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.7/go.mod h1:TFVe6Rr2joVLsYQ1ABACXgOC6lXip/qpX2x5jWg/A9w=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.8 h1:GNIdO14AHW5CgnzMml3Tg5Fy/+NqPQvnh1HsC1zpcPo=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.8/go.mod h1:UqRD9bBt15P0ofRyDZX6CfsIqPpzeHOhZKWzgSuAzpo=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 h1:aYToU0/iazkMY67/BYLt3r6/LT/mUtarLAF5mGof1Kg=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6/go.mod h1:rP1rEOKAGZoXp4iGDxSXFvODAtXpm34Egf0lL0eshaQ=
github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.7 h1:HLzjwQM9975FQWSF3uENDGHT1gFQm/q3QXu2BYIcI08=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.7/go.mod h1:lVxTdiiSHY3jb1aeg+BBFtDzZGSUCv6qaNOyEGCJ1AY=
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
github.com/aws/smithy-go v1.11.3 h1:DQixirEFM9IaKxX1olZ3ke3nvxRS2xMDteKIDWxozW8=
github.com/aws/smithy-go v1.11.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -226,8 +229,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.2.11 h1:gddwKS4W+zxfZdA0/dEMMjiruiQCCrG2iRbk0c1T13Y=
github.com/cockroachdb/cockroach-go/v2 v2.2.11/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
github.com/cockroachdb/cockroach-go/v2 v2.2.12 h1:yGneJ5OvdtAky2nD5BKTKmIqrcUlbrEIJ1ILHirnn3o=
github.com/cockroachdb/cockroach-go/v2 v2.2.12/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
github.com/coreos/go-oidc/v3 v3.2.0 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc=
github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@ -256,8 +259,8 @@ github.com/drakkan/crypto v0.0.0-20220527053356-5e1caf8ed0e1 h1:2sXHktgJwUnVAHbt
github.com/drakkan/crypto v0.0.0-20220527053356-5e1caf8ed0e1/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/drakkan/net v0.0.0-20220603083515-6ce0d6be4d73 h1:/+VzNg0b5DfRn5mMwgKfRI9fQv+Ak66i5rG7OmGaP30=
github.com/drakkan/net v0.0.0-20220603083515-6ce0d6be4d73/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
github.com/drakkan/net v0.0.0-20220609171611-ca8008b74f1f h1:DyQRIok4cgDwQUJd1E+KN53ANLdirNPqmx8ewqFN77U=
github.com/drakkan/net v0.0.0-20220609171611-ca8008b74f1f/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -270,7 +273,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
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.18.1-0.20220515214847-f96d31ec626e h1:D7/to1KmKRTTRQyExulywEVYKhB+/WOW3gqiKimrbXg=
@ -448,8 +450,8 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
@ -584,7 +586,6 @@ github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281/go.mod h1:lc+czkg
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
@ -593,8 +594,6 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/
github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@ -638,8 +637,8 @@ github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
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/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
@ -696,14 +695,14 @@ github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rs/zerolog v1.26.2-0.20220505171737-a4ec5e4cdd4b h1:wKjeedusHurN46dp/9kF0JLBh3YO54lu5juBX1oqJWE=
github.com/rs/zerolog v1.26.2-0.20220505171737-a4ec5e4cdd4b/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/sftpgo/sdk v0.1.1 h1:3vGGmRWLr+1vp+Z7OJG2LHt/u9MjTs3odZZtUcbfAsQ=
github.com/sftpgo/sdk v0.1.1/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d h1:gpshxOhLsGFbCy4ke96X8FVMy4xvXZQChSF7dikqxp4=
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA=
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -735,8 +734,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8=
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs=
@ -835,8 +835,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -858,8 +858,9 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -871,8 +872,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -890,7 +891,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -956,8 +956,9 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -976,8 +977,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -1040,8 +1041,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1049,8 +1050,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -1096,8 +1098,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw=
google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os=
google.golang.org/api v0.83.0 h1:pMvST+6v+46Gabac4zlJlalxZjCeRcepwg2EdBU+nCc=
google.golang.org/api v0.83.0/go.mod h1:CNywQoj/AfhTw26ZWAa6LwOv+6WFxHmeLPZq2uncLZk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -1199,9 +1201,9 @@ google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM=
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac h1:ByeiW1F67iV9o8ipGskA+HWzSkMbRJuKLlwCdPxzn7A=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

View file

@ -68,12 +68,15 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
currentSFTPPassword := folder.FsConfig.SFTPConfig.Password
currentSFTPKey := folder.FsConfig.SFTPConfig.PrivateKey
currentSFTPKeyPassphrase := folder.FsConfig.SFTPConfig.KeyPassphrase
currentHTTPPassword := folder.FsConfig.HTTPConfig.Password
currentHTTPAPIKey := folder.FsConfig.HTTPConfig.APIKey
folder.FsConfig.S3Config = vfs.S3FsConfig{}
folder.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
folder.FsConfig.GCSConfig = vfs.GCSFsConfig{}
folder.FsConfig.CryptConfig = vfs.CryptFsConfig{}
folder.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
folder.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
err = render.DecodeJSON(r.Body, &folder)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
@ -83,7 +86,8 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
folder.Name = name
folder.FsConfig.SetEmptySecretsIfNil()
updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials,
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase, currentHTTPPassword,
currentHTTPAPIKey)
err = dataprovider.UpdateFolder(&folder, users, groups, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))

View file

@ -73,12 +73,15 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
currentSFTPPassword := group.UserSettings.FsConfig.SFTPConfig.Password
currentSFTPKey := group.UserSettings.FsConfig.SFTPConfig.PrivateKey
currentSFTPKeyPassphrase := group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase
currentHTTPPassword := group.UserSettings.FsConfig.HTTPConfig.Password
currentHTTPAPIKey := group.UserSettings.FsConfig.HTTPConfig.APIKey
group.UserSettings.FsConfig.S3Config = vfs.S3FsConfig{}
group.UserSettings.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
group.UserSettings.FsConfig.GCSConfig = vfs.GCSFsConfig{}
group.UserSettings.FsConfig.CryptConfig = vfs.CryptFsConfig{}
group.UserSettings.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
group.UserSettings.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
err = render.DecodeJSON(r.Body, &group)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
@ -88,7 +91,8 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
group.Name = name
group.UserSettings.FsConfig.SetEmptySecretsIfNil()
updateEncryptedSecrets(&group.UserSettings.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
currentHTTPPassword, currentHTTPAPIKey)
err = dataprovider.UpdateGroup(&group, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))

View file

@ -135,6 +135,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
currentSFTPPassword := user.FsConfig.SFTPConfig.Password
currentSFTPKey := user.FsConfig.SFTPConfig.PrivateKey
currentSFTPKeyPassphrase := user.FsConfig.SFTPConfig.KeyPassphrase
currentHTTPPassword := user.FsConfig.HTTPConfig.Password
currentHTTPAPIKey := user.FsConfig.HTTPConfig.APIKey
user.Permissions = make(map[string][]string)
user.FsConfig.S3Config = vfs.S3FsConfig{}
@ -142,6 +144,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
user.FsConfig.GCSConfig = vfs.GCSFsConfig{}
user.FsConfig.CryptConfig = vfs.CryptFsConfig{}
user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{}
user.Filters.RecoveryCodes = nil
user.VirtualFolders = nil
@ -160,7 +163,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
user.Permissions = currentPermissions
}
updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
currentHTTPPassword, currentHTTPAPIKey)
err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
@ -232,7 +236,8 @@ func disconnectUser(username string) {
}
func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey *kms.Secret, currentSFTPKeyPassphrase *kms.Secret) {
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
currentHTTPPassword, currentHTTPAPIKey *kms.Secret) {
// we use the new access secret if plain or empty, otherwise the old value
switch fsConfig.Provider {
case sdk.S3FilesystemProvider:
@ -257,14 +262,31 @@ func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, cur
fsConfig.CryptConfig.Passphrase = currentCryptoPassphrase
}
case sdk.SFTPFilesystemProvider:
if fsConfig.SFTPConfig.Password.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.Password = currentSFTPPassword
}
if fsConfig.SFTPConfig.PrivateKey.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.PrivateKey = currentSFTPKey
}
if fsConfig.SFTPConfig.KeyPassphrase.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.KeyPassphrase = currentSFTPKeyPassphrase
}
updateSFTPFsEncryptedSecrets(fsConfig, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
case sdk.HTTPFilesystemProvider:
updateHTTPFsEncryptedSecrets(fsConfig, currentHTTPPassword, currentHTTPAPIKey)
}
}
func updateSFTPFsEncryptedSecrets(fsConfig *vfs.Filesystem, currentSFTPPassword, currentSFTPKey,
currentSFTPKeyPassphrase *kms.Secret,
) {
if fsConfig.SFTPConfig.Password.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.Password = currentSFTPPassword
}
if fsConfig.SFTPConfig.PrivateKey.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.PrivateKey = currentSFTPKey
}
if fsConfig.SFTPConfig.KeyPassphrase.IsNotPlainAndNotEmpty() {
fsConfig.SFTPConfig.KeyPassphrase = currentSFTPKeyPassphrase
}
}
func updateHTTPFsEncryptedSecrets(fsConfig *vfs.Filesystem, currentHTTPPassword, currentHTTPAPIKey *kms.Secret) {
if fsConfig.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
fsConfig.HTTPConfig.Password = currentHTTPPassword
}
if fsConfig.HTTPConfig.APIKey.IsNotPlainAndNotEmpty() {
fsConfig.HTTPConfig.APIKey = currentHTTPAPIKey
}
}

View file

@ -196,7 +196,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request
initialSize := int64(0)
truncatedSize := int64(0) // bytes truncated and not included in quota
if !isNewFile {
if vfs.IsLocalOrSFTPFs(fs) {
if vfs.HasTruncateSupport(fs) {
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck

View file

@ -2409,6 +2409,26 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
if assert.NoError(t, err) {
assert.Contains(t, string(resp), "invalid buffer_size")
}
u = getTestUser()
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
u.FsConfig.HTTPConfig.Endpoint = "http://foo\x7f.com/"
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
if assert.NoError(t, err) {
assert.Contains(t, string(resp), "invalid endpoint")
}
u.FsConfig.HTTPConfig.Endpoint = "http://127.0.0.1:9999/api/v1"
u.FsConfig.HTTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, "", "", "")
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
if assert.NoError(t, err) {
assert.Contains(t, string(resp), "invalid encrypted password")
}
u.FsConfig.HTTPConfig.Password = nil
u.FsConfig.HTTPConfig.APIKey = kms.NewSecret(sdkkms.SecretStatusRedacted, redactedSecret, "", "")
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
if assert.NoError(t, err) {
assert.Contains(t, string(resp), "cannot save a user with a redacted secret")
}
}
func TestUserRedactedPassword(t *testing.T) {
@ -3247,6 +3267,74 @@ func TestUserS3Config(t *testing.T) {
assert.NoError(t, err)
}
func TestHTTPFsConfig(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: "http://127.0.0.1/httpfs",
Username: defaultUsername,
},
Password: kms.NewPlainSecret(defaultPassword),
APIKey: kms.NewPlainSecret(defaultTokenAuthUser),
}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
initialPwdPayload := user.FsConfig.HTTPConfig.Password.GetPayload()
initialAPIKeyPayload := user.FsConfig.HTTPConfig.APIKey.GetPayload()
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())
assert.NotEmpty(t, initialPwdPayload)
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.NotEmpty(t, initialAPIKeyPayload)
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())
user.FsConfig.HTTPConfig.Password.SetStatus(sdkkms.SecretStatusSecretBox)
user.FsConfig.HTTPConfig.Password.SetAdditionalData(util.GenerateUniqueID())
user.FsConfig.HTTPConfig.Password.SetKey(util.GenerateUniqueID())
user.FsConfig.HTTPConfig.APIKey.SetStatus(sdkkms.SecretStatusSecretBox)
user.FsConfig.HTTPConfig.APIKey.SetAdditionalData(util.GenerateUniqueID())
user.FsConfig.HTTPConfig.APIKey.SetKey(util.GenerateUniqueID())
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())
assert.Equal(t, initialPwdPayload, user.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.Equal(t, initialAPIKeyPayload, user.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
// also test AddUser
u := getTestUser()
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: "http://127.0.0.1/httpfs",
Username: defaultUsername,
},
Password: kms.NewPlainSecret(defaultPassword),
APIKey: kms.NewPlainSecret(defaultTokenAuthUser),
}
user, _, err = httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())
assert.NotEmpty(t, user.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.NotEmpty(t, user.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func TestUserAzureBlobConfig(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
@ -3494,7 +3582,7 @@ func TestUserSFTPFs(t *testing.T) {
func TestUserHiddenFields(t *testing.T) {
// sensitive data must be hidden but not deleted from the dataprovider
usernames := []string{"user1", "user2", "user3", "user4", "user5"}
usernames := []string{"user1", "user2", "user3", "user4", "user5", "user6"}
u1 := getTestUser()
u1.Username = usernames[0]
u1.FsConfig.Provider = sdk.S3FilesystemProvider
@ -3542,9 +3630,23 @@ func TestUserHiddenFields(t *testing.T) {
user5, _, err := httpdtest.AddUser(u5, http.StatusCreated)
assert.NoError(t, err)
u6 := getTestUser()
u6.Username = usernames[5]
u6.FsConfig.Provider = sdk.HTTPFilesystemProvider
u6.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: "http://127.0.0.1/api/v1",
Username: defaultUsername,
},
Password: kms.NewPlainSecret(defaultPassword),
APIKey: kms.NewPlainSecret(defaultTokenAuthUser),
}
user6, _, err := httpdtest.AddUser(u6, http.StatusCreated)
assert.NoError(t, err)
users, _, err := httpdtest.GetUsers(0, 0, http.StatusOK)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(users), 5)
assert.GreaterOrEqual(t, len(users), 6)
for _, username := range usernames {
user, _, err := httpdtest.GetUserByUsername(username, http.StatusOK)
assert.NoError(t, err)
@ -3595,6 +3697,14 @@ func TestUserHiddenFields(t *testing.T) {
assert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetPayload())
assert.Equal(t, "/prefix", user5.FsConfig.SFTPConfig.Prefix)
user6, _, err = httpdtest.GetUserByUsername(user6.Username, http.StatusOK)
assert.NoError(t, err)
assert.Empty(t, user6.Password)
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetKey())
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.APIKey.GetPayload())
// finally check that we have all the data inside the data provider
user1, err = dataprovider.UserExists(user1.Username)
assert.NoError(t, err)
@ -3676,6 +3786,20 @@ func TestUserHiddenFields(t *testing.T) {
assert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetKey())
assert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())
user6, err = dataprovider.UserExists(user6.Username)
assert.NoError(t, err)
assert.NotEmpty(t, user6.Password)
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetKey())
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetStatus())
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetPayload())
err = user6.FsConfig.HTTPConfig.Password.Decrypt()
assert.NoError(t, err)
assert.Equal(t, sdkkms.SecretStatusPlain, user6.FsConfig.HTTPConfig.Password.GetStatus())
assert.Equal(t, u6.FsConfig.HTTPConfig.Password.GetPayload(), user6.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetKey())
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())
// update the GCS user and check that the credentials are preserved
user2.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
user2.FsConfig.GCSConfig.ACL = "private"
@ -3700,6 +3824,8 @@ func TestUserHiddenFields(t *testing.T) {
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user5, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user6, http.StatusOK)
assert.NoError(t, err)
}
func TestSecretObject(t *testing.T) {
@ -16951,6 +17077,125 @@ func TestWebUserGCSMock(t *testing.T) {
assert.NoError(t, err)
}
func TestWebUserHTTPFsMock(t *testing.T) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
assert.NoError(t, err)
user := getTestUser()
userAsJSON := getUserAsJSON(t, user)
req, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
assert.NoError(t, err)
setBearerForReq(req, apiToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr)
err = render.DecodeJSON(rr.Body, &user)
assert.NoError(t, err)
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: "https://127.0.0.1:9999/api/v1",
Username: defaultUsername,
SkipTLSVerify: true,
},
Password: kms.NewPlainSecret(defaultPassword),
APIKey: kms.NewPlainSecret(defaultTokenAuthPass),
}
form := make(url.Values)
form.Set(csrfFormToken, csrfToken)
form.Set("username", user.Username)
form.Set("password", redactedSecret)
form.Set("home_dir", user.HomeDir)
form.Set("uid", "0")
form.Set("gid", strconv.FormatInt(int64(user.GID), 10))
form.Set("max_sessions", strconv.FormatInt(int64(user.MaxSessions), 10))
form.Set("quota_size", strconv.FormatInt(user.QuotaSize, 10))
form.Set("quota_files", strconv.FormatInt(int64(user.QuotaFiles), 10))
form.Set("upload_bandwidth", "0")
form.Set("download_bandwidth", "0")
form.Set("upload_data_transfer", "0")
form.Set("download_data_transfer", "0")
form.Set("total_data_transfer", "0")
form.Set("external_auth_cache_time", "0")
form.Set("permissions", "*")
form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "2020-01-01 00:00:00")
form.Set("allowed_ip", "")
form.Set("denied_ip", "")
form.Set("fs_provider", "6")
form.Set("http_endpoint", user.FsConfig.HTTPConfig.Endpoint)
form.Set("http_username", user.FsConfig.HTTPConfig.Username)
form.Set("http_password", user.FsConfig.HTTPConfig.Password.GetPayload())
form.Set("http_api_key", user.FsConfig.HTTPConfig.APIKey.GetPayload())
form.Set("http_skip_tls_verify", "checked")
form.Set("pattern_path0", "/dir1")
form.Set("patterns0", "*.jpg,*.png")
form.Set("pattern_type0", "allowed")
form.Set("pattern_path1", "/dir2")
form.Set("patterns1", "*.zip")
form.Set("pattern_type1", "denied")
form.Set("max_upload_file_size", "0")
b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr)
// check the updated user
req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
var updateUser dataprovider.User
err = render.DecodeJSON(rr.Body, &updateUser)
assert.NoError(t, err)
assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)
assert.Equal(t, 2, len(updateUser.Filters.FilePatterns))
assert.Equal(t, user.FsConfig.HTTPConfig.Endpoint, updateUser.FsConfig.HTTPConfig.Endpoint)
assert.Equal(t, user.FsConfig.HTTPConfig.Username, updateUser.FsConfig.HTTPConfig.Username)
assert.Equal(t, user.FsConfig.HTTPConfig.SkipTLSVerify, updateUser.FsConfig.HTTPConfig.SkipTLSVerify)
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.HTTPConfig.Password.GetStatus())
assert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, updateUser.FsConfig.HTTPConfig.Password.GetKey())
assert.Empty(t, updateUser.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetKey())
assert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
// now check that a redacted password is not saved
form.Set("http_password", " "+redactedSecret+" ")
form.Set("http_api_key", redactedSecret)
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr)
req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
var lastUpdatedUser dataprovider.User
err = render.DecodeJSON(rr.Body, &lastUpdatedUser)
assert.NoError(t, err)
assert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetStatus())
assert.Equal(t, updateUser.FsConfig.HTTPConfig.Password.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetKey())
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.Equal(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetKey())
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
assert.NoError(t, err)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
}
func TestWebUserAzureBlobMock(t *testing.T) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
@ -17623,6 +17868,101 @@ func TestAddWebFoldersMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr)
}
func TestHTTPFsWebFolderMock(t *testing.T) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
assert.NoError(t, err)
mappedPath := filepath.Clean(os.TempDir())
folderName := filepath.Base(mappedPath)
httpfsConfig := vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: "https://127.0.0.1:9998/api/v1",
Username: folderName,
SkipTLSVerify: true,
},
Password: kms.NewPlainSecret(defaultPassword),
APIKey: kms.NewPlainSecret(defaultTokenAuthPass),
}
form := make(url.Values)
form.Set("mapped_path", mappedPath)
form.Set("name", folderName)
form.Set("fs_provider", "6")
form.Set("http_endpoint", httpfsConfig.Endpoint)
form.Set("http_username", "%name%")
form.Set("http_password", httpfsConfig.Password.GetPayload())
form.Set("http_api_key", httpfsConfig.APIKey.GetPayload())
form.Set("http_skip_tls_verify", "checked")
form.Set(csrfFormToken, csrfToken)
b, contentType, err := getMultipartFormData(form, "", "")
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, webFolderPath, &b)
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr := executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr)
// check
var folder vfs.BaseVirtualFolder
req, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = render.DecodeJSON(rr.Body, &folder)
assert.NoError(t, err)
assert.Equal(t, mappedPath, folder.MappedPath)
assert.Equal(t, folderName, folder.Name)
assert.Equal(t, sdk.HTTPFilesystemProvider, folder.FsConfig.Provider)
assert.Equal(t, httpfsConfig.Endpoint, folder.FsConfig.HTTPConfig.Endpoint)
assert.Equal(t, httpfsConfig.Username, folder.FsConfig.HTTPConfig.Username)
assert.Equal(t, httpfsConfig.SkipTLSVerify, folder.FsConfig.HTTPConfig.SkipTLSVerify)
assert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.HTTPConfig.Password.GetStatus())
assert.NotEmpty(t, folder.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, folder.FsConfig.HTTPConfig.Password.GetKey())
assert.Empty(t, folder.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.NotEmpty(t, folder.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, folder.FsConfig.HTTPConfig.APIKey.GetKey())
assert.Empty(t, folder.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
// update
form.Set("http_password", redactedSecret)
form.Set("http_api_key", redactedSecret)
b, contentType, err = getMultipartFormData(form, "", "")
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr)
// check
var updateFolder vfs.BaseVirtualFolder
req, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = render.DecodeJSON(rr.Body, &updateFolder)
assert.NoError(t, err)
assert.Equal(t, mappedPath, updateFolder.MappedPath)
assert.Equal(t, folderName, updateFolder.Name)
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateFolder.FsConfig.HTTPConfig.Password.GetStatus())
assert.Equal(t, folder.FsConfig.HTTPConfig.Password.GetPayload(), updateFolder.FsConfig.HTTPConfig.Password.GetPayload())
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.Password.GetKey())
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.Password.GetAdditionalData())
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateFolder.FsConfig.HTTPConfig.APIKey.GetStatus())
assert.Equal(t, folder.FsConfig.HTTPConfig.APIKey.GetPayload(), updateFolder.FsConfig.HTTPConfig.APIKey.GetPayload())
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.APIKey.GetKey())
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
// cleanup
req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)
setBearerForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
}
func TestS3WebFolderMock(t *testing.T) {
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)

View file

@ -393,8 +393,8 @@ func loadAdminTemplates(templatesPath string) {
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
"ListFSProviders": func() []sdk.FilesystemProvider {
return []sdk.FilesystemProvider{sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider,
sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider,
sdk.AzureBlobFilesystemProvider, sdk.SFTPFilesystemProvider,
sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider, sdk.AzureBlobFilesystemProvider,
sdk.SFTPFilesystemProvider, sdk.HTTPFilesystemProvider,
}
},
})
@ -1116,8 +1116,8 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
if util.Contains(hooks, "check_password_disabled") {
filters.Hooks.CheckPasswordDisabled = true
}
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
filters.DisableFsChecks = r.Form.Get("disable_fs_checks") != ""
filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
filters.StartDirectory = r.Form.Get("start_directory")
filters.MaxUploadFileSize = maxFileSize
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
@ -1223,7 +1223,7 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
fingerprintsFormValue := r.Form.Get("sftp_fingerprints")
config.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, "\n")
config.Prefix = r.Form.Get("sftp_prefix")
config.DisableCouncurrentReads = len(r.Form.Get("sftp_disable_concurrent_reads")) > 0
config.DisableCouncurrentReads = r.Form.Get("sftp_disable_concurrent_reads") != ""
config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid SFTP buffer size: %w", err)
@ -1231,6 +1231,16 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
return config, nil
}
func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
config := vfs.HTTPFsConfig{}
config.Endpoint = r.Form.Get("http_endpoint")
config.Username = r.Form.Get("http_username")
config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
config.Password = getSecretFromFormField(r, "http_password")
config.APIKey = getSecretFromFormField(r, "http_api_key")
return config
}
func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
var err error
config := vfs.AzBlobFsConfig{}
@ -1241,7 +1251,7 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
config.Endpoint = r.Form.Get("az_endpoint")
config.KeyPrefix = r.Form.Get("az_key_prefix")
config.AccessTier = r.Form.Get("az_access_tier")
config.UseEmulator = len(r.Form.Get("az_use_emulator")) > 0
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid azure upload part size: %w", err)
@ -1291,6 +1301,8 @@ func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
return fs, err
}
fs.SFTPConfig = config
case sdk.HTTPFilesystemProvider:
fs.HTTPConfig = getHTTPFsConfig(r)
}
return fs, nil
}
@ -1311,7 +1323,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
admin.Email = r.Form.Get("email")
admin.Status = status
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
admin.AdditionalInfo = r.Form.Get("additional_info")
admin.Description = r.Form.Get("description")
return admin, nil
@ -1342,6 +1354,8 @@ func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVi
folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)
case sdk.SFTPFilesystemProvider:
folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)
case sdk.HTTPFilesystemProvider:
folder.FsConfig.HTTPConfig = getHTTPFsFromTemplate(folder.FsConfig.HTTPConfig, replacements)
}
return folder
@ -1392,6 +1406,11 @@ func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]st
return fsConfig
}
func getHTTPFsFromTemplate(fsConfig vfs.HTTPFsConfig, replacements map[string]string) vfs.HTTPFsConfig {
fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
return fsConfig
}
func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {
user.Username = template.Username
user.Password = template.Password
@ -1425,6 +1444,8 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
case sdk.SFTPFilesystemProvider:
user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
case sdk.HTTPFilesystemProvider:
user.FsConfig.HTTPConfig = getHTTPFsFromTemplate(user.FsConfig.HTTPConfig, replacements)
}
return user
@ -1699,7 +1720,7 @@ func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.R
s.renderProfilePage(w, r, err.Error())
return
}
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
admin.Email = r.Form.Get("email")
admin.Description = r.Form.Get("description")
err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr)
@ -2203,7 +2224,8 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
}
updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase)
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase,
user.FsConfig.HTTPConfig.Password, user.FsConfig.HTTPConfig.APIKey)
updatedUser = getUserFromTemplate(updatedUser, userTemplateFields{
Username: updatedUser.Username,
@ -2337,7 +2359,8 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
updatedFolder.FsConfig.SetEmptySecretsIfNil()
updateEncryptedSecrets(&updatedFolder.FsConfig, folder.FsConfig.S3Config.AccessSecret, folder.FsConfig.AzBlobConfig.AccountKey,
folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase)
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase,
folder.FsConfig.HTTPConfig.Password, folder.FsConfig.HTTPConfig.APIKey)
updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)
@ -2502,7 +2525,8 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
group.UserSettings.FsConfig.AzBlobConfig.AccountKey, group.UserSettings.FsConfig.AzBlobConfig.SASURL,
group.UserSettings.FsConfig.GCSConfig.Credentials, group.UserSettings.FsConfig.CryptConfig.Passphrase,
group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey,
group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase)
group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase, group.UserSettings.FsConfig.HTTPConfig.Password,
group.UserSettings.FsConfig.HTTPConfig.APIKey)
err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr)
if err != nil {

View file

@ -1160,7 +1160,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
user.PublicKeys = r.Form["public_keys"]
}
if userMerged.CanChangeAPIKeyAuth() {
user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
user.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
}
if userMerged.CanChangeInfo() {
user.Email = r.Form.Get("email")

View file

@ -1426,7 +1426,10 @@ func compareFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
if err := checkEncryptedSecret(expected.CryptConfig.Passphrase, actual.CryptConfig.Passphrase); err != nil {
return err
}
return compareSFTPFsConfig(expected, actual)
if err := compareSFTPFsConfig(expected, actual); err != nil {
return err
}
return compareHTTPFsConfig(expected, actual)
}
func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error { //nolint:gocyclo
@ -1502,6 +1505,25 @@ func compareGCSConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
return nil
}
func compareHTTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
if expected.HTTPConfig.Endpoint != actual.HTTPConfig.Endpoint {
return errors.New("HTTPFs endpoint mismatch")
}
if expected.HTTPConfig.Username != actual.HTTPConfig.Username {
return errors.New("HTTPFs username mismatch")
}
if expected.HTTPConfig.SkipTLSVerify != actual.HTTPConfig.SkipTLSVerify {
return errors.New("HTTPFs skip_tls_verify mismatch")
}
if err := checkEncryptedSecret(expected.HTTPConfig.Password, actual.HTTPConfig.Password); err != nil {
return fmt.Errorf("HTTPFs password mismatch: %v", err)
}
if err := checkEncryptedSecret(expected.HTTPConfig.APIKey, actual.HTTPConfig.APIKey); err != nil {
return fmt.Errorf("HTTPFs API key mismatch: %v", err)
}
return nil
}
func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
if expected.SFTPConfig.Endpoint != actual.SFTPConfig.Endpoint {
return errors.New("SFTPFs endpoint mismatch")

495
httpdtest/httpfsimpl.go Normal file
View file

@ -0,0 +1,495 @@
package httpdtest
import (
"context"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/shirou/gopsutil/v3/disk"
"github.com/drakkan/sftpgo/v2/util"
)
const (
statPath = "/api/v1/stat"
openPath = "/api/v1/open"
createPath = "/api/v1/create"
renamePath = "/api/v1/rename"
removePath = "/api/v1/remove"
mkdirPath = "/api/v1/mkdir"
chmodPath = "/api/v1/chmod"
chtimesPath = "/api/v1/chtimes"
truncatePath = "/api/v1/truncate"
readdirPath = "/api/v1/readdir"
dirsizePath = "/api/v1/dirsize"
mimetypePath = "/api/v1/mimetype"
statvfsPath = "/api/v1/statvfs"
)
// StartTestHTTPFs starts a test HTTP service that implements httpfs
func StartTestHTTPFs(port int) error {
fs := httpFsImpl{
port: port,
}
return fs.Run()
}
type httpFsImpl struct {
router *chi.Mux
basePath string
port int
}
type apiResponse struct {
Error string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
}
func (fs *httpFsImpl) sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
var errorString string
if err != nil {
errorString = err.Error()
}
resp := apiResponse{
Error: errorString,
Message: message,
}
ctx := context.WithValue(r.Context(), render.StatusCtxKey, code)
render.JSON(w, r.WithContext(ctx), resp)
}
func (fs *httpFsImpl) getUsername(r *http.Request) (string, error) {
username, _, ok := r.BasicAuth()
if !ok || username == "" {
return "", os.ErrPermission
}
rootPath := filepath.Join(fs.basePath, username)
_, err := os.Stat(rootPath)
if errors.Is(err, os.ErrNotExist) {
err = os.MkdirAll(rootPath, os.ModePerm)
if err != nil {
return username, err
}
}
return username, nil
}
func (fs *httpFsImpl) getRespStatus(err error) int {
if errors.Is(err, os.ErrPermission) {
return http.StatusForbidden
}
if errors.Is(err, os.ErrNotExist) {
return http.StatusNotFound
}
return http.StatusInternalServerError
}
func (fs *httpFsImpl) stat(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
info, err := os.Stat(fsPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
render.JSON(w, r, getStatFromInfo(info))
}
func (fs *httpFsImpl) open(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
var offset int64
if r.URL.Query().Has("offset") {
offset, err = strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
f, err := os.Open(fsPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
defer f.Close()
if offset > 0 {
_, err = f.Seek(offset, io.SeekStart)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
}
ctype := mime.TypeByExtension(filepath.Ext(name))
if ctype != "" {
ctype = "application/octet-stream"
}
w.Header().Set("Content-Type", ctype)
_, err = io.Copy(w, f)
if err != nil {
panic(http.ErrAbortHandler)
}
}
func (fs *httpFsImpl) create(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
if r.URL.Query().Has("flags") {
openFlags, err := strconv.ParseInt(r.URL.Query().Get("flags"), 10, 32)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
if openFlags > 0 {
flags = int(openFlags)
}
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
f, err := os.OpenFile(fsPath, flags, 0666)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
defer f.Close()
_, err = io.Copy(f, r.Body)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "upload OK", http.StatusOK)
}
func (fs *httpFsImpl) rename(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
target := r.URL.Query().Get("target")
if target == "" {
fs.sendAPIResponse(w, r, nil, "target path cannot be empty", http.StatusBadRequest)
return
}
name := getNameURLParam(r)
sourcePath := filepath.Join(fs.basePath, username, name)
targetPath := filepath.Join(fs.basePath, username, target)
err = os.Rename(sourcePath, targetPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "rename OK", http.StatusOK)
}
func (fs *httpFsImpl) remove(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
err = os.Remove(fsPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "remove OK", http.StatusOK)
}
func (fs *httpFsImpl) mkdir(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
err = os.Mkdir(fsPath, os.ModePerm)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "mkdir OK", http.StatusOK)
}
func (fs *httpFsImpl) chmod(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
mode, err := strconv.ParseUint(r.URL.Query().Get("mode"), 10, 32)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
err = os.Chmod(fsPath, os.FileMode(mode))
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "chmod OK", http.StatusOK)
}
func (fs *httpFsImpl) chtimes(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
atime, err := time.Parse(time.RFC3339, r.URL.Query().Get("access_time"))
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
mtime, err := time.Parse(time.RFC3339, r.URL.Query().Get("modification_time"))
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
err = os.Chtimes(fsPath, atime, mtime)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "chtimes OK", http.StatusOK)
}
func (fs *httpFsImpl) truncate(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
size, err := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
err = os.Truncate(fsPath, size)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
fs.sendAPIResponse(w, r, nil, "chmod OK", http.StatusOK)
}
func (fs *httpFsImpl) readdir(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
f, err := os.Open(fsPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
result := make([]map[string]any, 0, len(list))
for _, fi := range list {
result = append(result, getStatFromInfo(fi))
}
render.JSON(w, r, result)
}
func (fs *httpFsImpl) dirsize(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
info, err := os.Stat(fsPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
numFiles := 0
size := int64(0)
if info.IsDir() {
err = filepath.Walk(fsPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info != nil && info.Mode().IsRegular() {
size += info.Size()
numFiles++
}
return err
})
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
}
render.JSON(w, r, map[string]any{
"files": numFiles,
"size": size,
})
}
func (fs *httpFsImpl) mimetype(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
f, err := os.OpenFile(fsPath, os.O_RDONLY, 0)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
defer f.Close()
var buf [512]byte
n, err := io.ReadFull(f, buf[:])
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
ctype := http.DetectContentType(buf[:n])
render.JSON(w, r, map[string]any{
"mime": ctype,
})
}
func (fs *httpFsImpl) statvfs(w http.ResponseWriter, r *http.Request) {
username, err := fs.getUsername(r)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
name := getNameURLParam(r)
fsPath := filepath.Join(fs.basePath, username, name)
usage, err := disk.Usage(fsPath)
if err != nil {
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
return
}
// we assume block size = 4096
bsize := uint64(4096)
blocks := usage.Total / bsize
bfree := usage.Free / bsize
files := usage.InodesTotal
ffree := usage.InodesFree
if files == 0 {
// these assumptions are wrong but still better than returning 0
files = blocks / 4
ffree = bfree / 4
}
render.JSON(w, r, map[string]any{
"bsize": bsize,
"frsize": bsize,
"blocks": blocks,
"bfree": bfree,
"bavail": bfree,
"files": files,
"ffree": ffree,
"favail": ffree,
"namemax": 255,
})
}
func (fs *httpFsImpl) configureRouter() {
fs.router = chi.NewRouter()
fs.router.Use(middleware.Recoverer)
fs.router.Get(statPath+"/{name}", fs.stat)
fs.router.Get(openPath+"/{name}", fs.open)
fs.router.Post(createPath+"/{name}", fs.create)
fs.router.Patch(renamePath+"/{name}", fs.rename)
fs.router.Delete(removePath+"/{name}", fs.remove)
fs.router.Post(mkdirPath+"/{name}", fs.mkdir)
fs.router.Patch(chmodPath+"/{name}", fs.chmod)
fs.router.Patch(chtimesPath+"/{name}", fs.chtimes)
fs.router.Patch(truncatePath+"/{name}", fs.truncate)
fs.router.Get(readdirPath+"/{name}", fs.readdir)
fs.router.Get(dirsizePath+"/{name}", fs.dirsize)
fs.router.Get(mimetypePath+"/{name}", fs.mimetype)
fs.router.Get(statvfsPath+"/{name}", fs.statvfs)
}
func (fs *httpFsImpl) Run() error {
fs.basePath = filepath.Join(os.TempDir(), "httpfs")
if err := os.RemoveAll(fs.basePath); err != nil {
return err
}
if err := os.MkdirAll(fs.basePath, os.ModePerm); err != nil {
return err
}
fs.configureRouter()
httpServer := http.Server{
Addr: fmt.Sprintf(":%d", fs.port),
Handler: fs.router,
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 16, // 64KB
}
return httpServer.ListenAndServe()
}
func getStatFromInfo(info os.FileInfo) map[string]any {
return map[string]any{
"name": info.Name(),
"size": info.Size(),
"mode": info.Mode(),
"last_modified": info.ModTime(),
}
}
func getNameURLParam(r *http.Request) string {
v := chi.URLParam(r, "name")
unescaped, err := url.PathUnescape(v)
if err != nil {
return util.CleanPath(v)
}
return util.CleanPath(unescaped)
}

601
openapi/httpfs.yaml Normal file
View file

@ -0,0 +1,601 @@
openapi: 3.0.3
tags:
- name: fs
info:
title: SFTPGo HTTPFs
description: 'SFTPGo HTTP Filesystem API'
version: 0.1.0
servers:
- url: /v1
security:
- ApiKeyAuth: []
- BasicAuth: []
paths:
/stat/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
get:
tags:
- fs
summary: Describes the named object
operationId: stat
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/FileInfo'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/open/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
- name: offset
in: query
description: 'offset, in bytes, from the start. If not specified 0 must be assumed'
required: false
schema:
type: integer
format: int64
get:
tags:
- fs
summary: Opens the named file for reading
operationId: open
responses:
'200':
description: successful operation
content:
'*/*':
schema:
type: string
format: binary
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/create/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
- name: flags
in: query
description: 'flags to use for opening the file, if omitted O_RDWR|O_CREATE|O_TRUNC must be assumed. Supported flags: https://pkg.go.dev/os#pkg-constants'
required: false
schema:
type: integer
format: int32
post:
tags:
- fs
summary: Creates or opens the named file for writing
operationId: create
requestBody:
content:
'*/*':
schema:
type: string
format: binary
required: true
responses:
201:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/rename/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
- name: target
in: query
description: target name
required: true
schema:
type: string
patch:
tags:
- fs
summary: Renames (moves) source to target
operationId: rename
responses:
200:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/remove/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
delete:
tags:
- fs
summary: Removes the named file or (empty) directory.
operationId: delete
responses:
200:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/mkdir/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
post:
tags:
- fs
summary: Creates a new directory with the specified name
operationId: mkdir
responses:
200:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/chmod/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
- name: mode
in: query
required: true
schema:
type: integer
patch:
tags:
- fs
summary: Changes the mode of the named file
operationId: chmod
responses:
200:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/chtimes/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
- name: access_time
in: query
required: true
schema:
type: string
format: date-time
- name: modification_time
in: query
required: true
schema:
type: string
format: date-time
patch:
tags:
- fs
summary: Changes the access and modification time of the named file
operationId: chtimes
responses:
200:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/truncate/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
- name: size
in: query
required: true
description: 'new file size in bytes'
schema:
type: integer
format: int64
patch:
tags:
- fs
summary: Changes the size of the named file
operationId: truncate
responses:
200:
$ref: '#/components/responses/OKResponse'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/readdir/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
get:
tags:
- fs
summary: Reads the named directory and returns the contents
operationId: readdir
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FileInfo'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/dirsize/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
get:
tags:
- fs
summary: Returns the number of files and the size for the named directory including any sub-directory
operationId: dirsize
responses:
200:
description: successful operation
content:
application/json:
schema:
type: object
properties:
files:
type: integer
description: 'Total number of files'
size:
type: integer
format: int64
description: 'Total size of files'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/mimetype/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
get:
tags:
- fs
summary: Returns the mime type for the named file
operationId: mimetype
responses:
200:
description: successful operation
content:
application/json:
schema:
type: object
properties:
mime:
type: string
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
/statvfs/{name}:
parameters:
- name: name
in: path
description: object name
required: true
schema:
type: string
get:
tags:
- fs
summary: Returns the VFS stats for the specified path
operationId: statvfs
responses:
200:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/StatVFS'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
500:
$ref: '#/components/responses/InternalServerError'
501:
$ref: '#/components/responses/NotImplemented'
default:
$ref: '#/components/responses/DefaultResponse'
components:
responses:
OKResponse:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
Forbidden:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
NotFound:
description: Not Found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
NotImplemented:
description: Not Implemented
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
Conflict:
description: Conflict
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
RequestEntityTooLarge:
description: Request Entity Too Large, max allowed size exceeded
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
InternalServerError:
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
DefaultResponse:
description: Unexpected Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
schemas:
ApiResponse:
type: object
properties:
message:
type: string
description: 'message, can be empty'
error:
type: string
description: error description if any
FileInfo:
type: object
properties:
name:
type: string
description: base name of the file
size:
type: integer
format: int64
description: length in bytes for regular files; system-dependent for others
mode:
type: integer
description: |
File mode and permission bits. More details here: https://golang.org/pkg/io/fs/#FileMode.
Let's see some examples:
- for a directory mode&2147483648 != 0
- for a symlink mode&134217728 != 0
- for a regular file mode&2401763328 == 0
last_modified:
type: string
format: date-time
StatVFS:
type: object
properties:
bsize:
type: integer
description: file system block size
frsize:
type: integer
description: fundamental fs block size
blocks:
type: integer
description: number of blocks
bfree:
type: integer
description: free blocks in file system
bavail:
type: integer
description: free blocks for non-root
files:
type: integer
description: total file inodes
ffree:
type: integer
description: free file inodes
favail:
type: integer
description: free file inodes for non-root
fsid:
type: integer
description: file system id
flag:
type: integer
description: bit mask of f_flag values
namemax:
type: integer
description: maximum filename length
securitySchemes:
BasicAuth:
type: http
scheme: basic
ApiKeyAuth:
type: apiKey
in: header
name: X-API-KEY

View file

@ -26,7 +26,7 @@ info:
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
version: 2.3.0-dev
version: 2.3.1-dev
contact:
name: API support
url: 'https://github.com/drakkan/sftpgo'
@ -4473,6 +4473,7 @@ components:
- 3
- 4
- 5
- 6
description: |
Filesystem providers:
* `0` - Local filesystem
@ -4481,6 +4482,7 @@ components:
* `3` - Azure Blob Storage
* `4` - Local filesystem encrypted
* `5` - SFTP
* `6` - HTTP filesystem
LoginMethods:
type: string
enum:
@ -5054,6 +5056,20 @@ components:
maximum: 16
example: 2
description: The size of the buffer (in MB) to use for transfers. By enabling buffering, the reads and writes, from/to the remote SFTP server, are split in multiple concurrent requests and this allows data to be transferred at a faster rate, over high latency networks, by overlapping round-trip times. With buffering enabled, resuming uploads is not supported and a file cannot be opened for both reading and writing at the same time. 0 means disabled.
HTTPFsConfig:
type: object
properties:
endpoint:
type: string
description: 'HTTP/S endpoint URL. SFTPGo will use this URL as base, for example for the `stat` API, SFTPGo will add `/stat/{name}`'
username:
type: string
password:
$ref: '#/components/schemas/Secret'
api_key:
$ref: '#/components/schemas/Secret'
skip_tls_verify:
type: boolean
FilesystemConfig:
type: object
properties:
@ -5069,6 +5085,8 @@ components:
$ref: '#/components/schemas/CryptFsConfig'
sftpconfig:
$ref: '#/components/schemas/SFTPFsConfig'
httpconfig:
$ref: '#/components/schemas/HTTPFsConfig'
description: Storage filesystem details
BaseVirtualFolder:
type: object

View file

@ -1,3 +1,9 @@
sftpgo (2.3.1-1ppa1) bionic; urgency=medium
* New upstream release
-- Nicola Murino <nicola.murino@gmail.com> Fri, 10 Jun 2022 19:48:21 +0200
sftpgo (2.3.0-1ppa1) bionic; urgency=medium
* New upstream release

View file

@ -229,15 +229,20 @@ func (s *Service) advertiseServices(advertiseService, advertiseCredentials bool)
}
func (s *Service) getPortableDirToServe() string {
var dirToServe string
if s.PortableUser.FsConfig.Provider == sdk.S3FilesystemProvider {
dirToServe = s.PortableUser.FsConfig.S3Config.KeyPrefix
} else if s.PortableUser.FsConfig.Provider == sdk.GCSFilesystemProvider {
dirToServe = s.PortableUser.FsConfig.GCSConfig.KeyPrefix
} else {
dirToServe = s.PortableUser.HomeDir
switch s.PortableUser.FsConfig.Provider {
case sdk.S3FilesystemProvider:
return s.PortableUser.FsConfig.S3Config.KeyPrefix
case sdk.GCSFilesystemProvider:
return s.PortableUser.FsConfig.GCSConfig.KeyPrefix
case sdk.AzureBlobFilesystemProvider:
return s.PortableUser.FsConfig.AzBlobConfig.KeyPrefix
case sdk.SFTPFilesystemProvider:
return s.PortableUser.FsConfig.SFTPConfig.Prefix
case sdk.HTTPFilesystemProvider:
return "/"
default:
return s.PortableUser.HomeDir
}
return dirToServe
}
// configures the portable user and return the printable password if any
@ -266,43 +271,36 @@ func (s *Service) configurePortableSecrets() {
switch s.PortableUser.FsConfig.Provider {
case sdk.S3FilesystemProvider:
payload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload()
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.S3Config.AccessSecret = getSecretFromString(payload)
case sdk.GCSFilesystemProvider:
payload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload()
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.GCSConfig.Credentials = getSecretFromString(payload)
case sdk.AzureBlobFilesystemProvider:
payload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = getSecretFromString(payload)
payload = s.PortableUser.FsConfig.AzBlobConfig.SASURL.GetPayload()
s.PortableUser.FsConfig.AzBlobConfig.SASURL = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.AzBlobConfig.SASURL = getSecretFromString(payload)
case sdk.CryptedFilesystemProvider:
payload := s.PortableUser.FsConfig.CryptConfig.Passphrase.GetPayload()
s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.CryptConfig.Passphrase = getSecretFromString(payload)
case sdk.SFTPFilesystemProvider:
payload := s.PortableUser.FsConfig.SFTPConfig.Password.GetPayload()
s.PortableUser.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.SFTPConfig.Password = getSecretFromString(payload)
payload = s.PortableUser.FsConfig.SFTPConfig.PrivateKey.GetPayload()
s.PortableUser.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
if payload != "" {
s.PortableUser.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(payload)
}
s.PortableUser.FsConfig.SFTPConfig.PrivateKey = getSecretFromString(payload)
payload = s.PortableUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload()
s.PortableUser.FsConfig.SFTPConfig.KeyPassphrase = getSecretFromString(payload)
case sdk.HTTPFilesystemProvider:
payload := s.PortableUser.FsConfig.HTTPConfig.Password.GetPayload()
s.PortableUser.FsConfig.HTTPConfig.Password = getSecretFromString(payload)
payload = s.PortableUser.FsConfig.HTTPConfig.APIKey.GetPayload()
s.PortableUser.FsConfig.HTTPConfig.APIKey = getSecretFromString(payload)
}
}
func getSecretFromString(payload string) *kms.Secret {
if payload != "" {
return kms.NewPlainSecret(payload)
}
return kms.NewEmptySecret()
}

View file

@ -443,7 +443,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
}
initialSize = fileSize
} else {
if vfs.IsLocalOrSFTPFs(fs) && isTruncate {
if isTruncate && vfs.HasTruncateSupport(fs) {
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck

294
sftpd/httpfs_test.go Normal file
View file

@ -0,0 +1,294 @@
package sftpd_test
import (
"fmt"
"io/fs"
"math"
"net/http"
"os"
"path"
"path/filepath"
"testing"
"time"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/httpdtest"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/vfs"
)
const (
httpFsPort = 12345
defaultHTTPFsUsername = "httpfs_user"
)
func TestBasicHTTPFsHandling(t *testing.T) {
usePubKey := true
u := getTestUserWithHTTPFs(usePubKey)
u.QuotaSize = 6553600
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()
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
expectedQuotaSize := user.UsedQuotaSize + testFileSize*2
expectedQuotaFiles := user.UsedQuotaFiles + 2
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = sftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client)
assert.Error(t, err)
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
info, err := client.Stat(testFileName)
if assert.NoError(t, err) {
assert.Equal(t, testFileSize, info.Size())
}
contents, err := client.ReadDir("/")
assert.NoError(t, err)
if assert.Len(t, contents, 1) {
assert.Equal(t, testFileName, contents[0].Name())
}
dirName := "test dirname"
err = client.Mkdir(dirName)
assert.NoError(t, err)
contents, err = client.ReadDir(".")
assert.NoError(t, err)
assert.Len(t, contents, 2)
contents, err = client.ReadDir(dirName)
assert.NoError(t, err)
assert.Len(t, contents, 0)
err = sftpUploadFile(testFilePath, path.Join(dirName, testFileName), testFileSize, client)
assert.NoError(t, err)
contents, err = client.ReadDir(dirName)
assert.NoError(t, err)
assert.Len(t, contents, 1)
dirRenamed := dirName + "_renamed"
err = client.Rename(dirName, dirRenamed)
assert.NoError(t, err)
info, err = client.Stat(dirRenamed)
if assert.NoError(t, err) {
assert.True(t, info.IsDir())
}
// mode 0666 and 0444 works on Windows too
newPerm := os.FileMode(0444)
err = client.Chmod(testFileName, newPerm)
assert.NoError(t, err)
info, err = client.Stat(testFileName)
assert.NoError(t, err)
assert.Equal(t, newPerm, info.Mode().Perm())
newPerm = os.FileMode(0666)
err = client.Chmod(testFileName, newPerm)
assert.NoError(t, err)
info, err = client.Stat(testFileName)
assert.NoError(t, err)
assert.Equal(t, newPerm, info.Mode().Perm())
// chtimes
acmodTime := time.Now().Add(-36 * time.Hour)
err = client.Chtimes(testFileName, acmodTime, acmodTime)
assert.NoError(t, err)
info, err = client.Stat(testFileName)
if assert.NoError(t, err) {
diff := math.Abs(info.ModTime().Sub(acmodTime).Seconds())
assert.LessOrEqual(t, diff, float64(1))
}
_, err = client.StatVFS("/")
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
// execute a quota scan
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
assert.NoError(t, err)
assert.Eventually(t, func() bool {
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
if err == nil {
return len(scans) == 0
}
return false
}, 1*time.Second, 50*time.Millisecond)
err = client.Remove(testFileName)
assert.NoError(t, err)
_, err = client.Lstat(testFileName)
assert.Error(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
// truncate
err = client.Truncate(path.Join(dirRenamed, testFileName), 100)
assert.NoError(t, err)
info, err = client.Stat(path.Join(dirRenamed, testFileName))
if assert.NoError(t, err) {
assert.Equal(t, int64(100), info.Size())
}
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, int64(100), user.UsedQuotaSize)
// update quota
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
assert.NoError(t, err)
assert.Eventually(t, func() bool {
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
if err == nil {
return len(scans) == 0
}
return false
}, 1*time.Second, 50*time.Millisecond)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, int64(100), user.UsedQuotaSize)
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 TestHTTPFsVirtualFolder(t *testing.T) {
usePubKey := false
u := getTestUser(usePubKey)
folderName := "httpfsfolder"
vdirPath := "/vdir/http fs"
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: folderName,
FsConfig: vfs.Filesystem{
Provider: sdk.HTTPFilesystemProvider,
HTTPConfig: vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
Username: defaultHTTPFsUsername,
},
},
},
},
VirtualPath: vdirPath,
})
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()
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = sftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client)
assert.NoError(t, err)
_, err = client.Stat(path.Join(vdirPath, testFileName))
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
assert.NoError(t, err)
}
func TestHTTPFsWalk(t *testing.T) {
user := getTestUserWithHTTPFs(false)
httpFs, err := user.GetFilesystem("")
require.NoError(t, err)
basePath := filepath.Join(os.TempDir(), "httpfs", user.FsConfig.HTTPConfig.Username)
err = os.RemoveAll(basePath)
assert.NoError(t, err)
var walkedPaths []string
err = httpFs.Walk("/", func(walkedPath string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
return nil
})
require.NoError(t, err)
require.Len(t, walkedPaths, 1)
require.Contains(t, walkedPaths, "/")
// now add some files/folders
for i := 0; i < 10; i++ {
err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("file%d", i)), nil, os.ModePerm)
assert.NoError(t, err)
err = os.Mkdir(filepath.Join(basePath, fmt.Sprintf("dir%d", i)), os.ModePerm)
assert.NoError(t, err)
for j := 0; j < 5; j++ {
err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)), nil, os.ModePerm)
assert.NoError(t, err)
}
}
walkedPaths = nil
err = httpFs.Walk("/", func(walkedPath string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
return nil
})
require.NoError(t, err)
require.Len(t, walkedPaths, 71)
require.Contains(t, walkedPaths, "/")
for i := 0; i < 10; i++ {
require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("file%d", i)))
require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i)))
for j := 0; j < 5; j++ {
require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)))
}
}
err = os.RemoveAll(basePath)
assert.NoError(t, err)
}
func getTestUserWithHTTPFs(usePubKey bool) dataprovider.User {
u := getTestUser(usePubKey)
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
Username: defaultHTTPFsUsername,
},
}
return u
}
func startHTTPFs() {
go func() {
if err := httpdtest.StartTestHTTPFs(httpFsPort); err != nil {
logger.ErrorToConsole("could not start HTTPfs test server: %v", err)
os.Exit(1)
}
}()
waitTCPListening(fmt.Sprintf(":%d", httpFsPort))
}

View file

@ -243,7 +243,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
initialSize := int64(0)
truncatedSize := int64(0) // bytes truncated and not included in quota
if !isNewFile {
if vfs.IsLocalOrSFTPFs(fs) {
if vfs.HasTruncateSupport(fs) {
vfolder, err := c.connection.User.GetVirtualFolderForPath(path.Dir(requestPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck

View file

@ -331,6 +331,7 @@ func TestMain(m *testing.M) {
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
getHostKeysFingerprints(sftpdConf.HostKeys)
startHTTPFs()
exitCode := m.Run()
os.Remove(logFilePath)

View file

@ -593,37 +593,19 @@ func (c *sshCommand) checkRecursiveCopyPermissions(fsSrc vfs.Fs, fsDst vfs.Fs, f
if !c.connection.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(sshDestPath)) {
return common.ErrPermissionDenied
}
dstPerms := []string{
dataprovider.PermCreateDirs,
dataprovider.PermCreateSymlinks,
dataprovider.PermUpload,
}
err := fsSrc.Walk(fsSourcePath, func(walkedPath string, info os.FileInfo, err error) error {
return fsSrc.Walk(fsSourcePath, func(walkedPath string, info os.FileInfo, err error) error {
if err != nil {
return c.connection.GetFsError(fsSrc, err)
}
fsDstSubPath := strings.Replace(walkedPath, fsSourcePath, fsDestPath, 1)
sshSrcSubPath := fsSrc.GetRelativePath(walkedPath)
sshDstSubPath := fsDst.GetRelativePath(fsDstSubPath)
// If the current dir has no subdirs with defined permissions inside it
// and it has all the possible permissions we can stop scanning
if !c.connection.User.HasPermissionsInside(path.Dir(sshSrcSubPath)) &&
!c.connection.User.HasPermissionsInside(path.Dir(sshDstSubPath)) {
if c.connection.User.HasPerm(dataprovider.PermListItems, path.Dir(sshSrcSubPath)) &&
c.connection.User.HasPerms(dstPerms, path.Dir(sshDstSubPath)) {
return common.ErrSkipPermissionsCheck
}
}
if !c.hasCopyPermissions(sshSrcSubPath, sshDstSubPath, info) {
return common.ErrPermissionDenied
}
return nil
})
if err == common.ErrSkipPermissionsCheck {
err = nil
}
return err
}
func (c *sshCommand) checkCopyPermissions(fsSrc vfs.Fs, fsDst vfs.Fs, fsSourcePath, fsDestPath, sshSourcePath, sshDestPath string, info os.FileInfo) error {

View file

@ -465,6 +465,44 @@
<label for="idDisableConcurrentReads" class="form-check-label">Disable concurrent reads</label>
</div>
</div>
<div class="form-group row fsconfig fsconfig-httpfs">
<label for="idHTTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idHTTPEndpoint" name="http_endpoint" placeholder=""
value="{{.HTTPConfig.Endpoint}}" maxlength="255">
</div>
</div>
<div class="form-group row fsconfig fsconfig-httpfs">
<label for="idHTTPUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-3">
<input type="text" class="form-control" id="idHTTPUsername" name="http_username" placeholder=""
value="{{.HTTPConfig.Username}}" maxlength="255">
</div>
<div class="col-sm-2"></div>
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-3">
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
value="{{if .HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.Password.GetPayload}}{{end}}">
</div>
</div>
<div class="form-group row fsconfig fsconfig-httpfs">
<label for="idHTTPAPIKey" class="col-sm-2 col-form-label">API Key</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idHTTPAPIKey" name="http_api_key" placeholder=""
value="{{if .HTTPConfig.APIKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.APIKey.GetPayload}}{{end}}">
</div>
</div>
<div class="form-group fsconfig fsconfig-httpfs">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idHTTPSkipTLSVerify"
name="http_skip_tls_verify" {{if .HTTPConfig.SkipTLSVerify}}checked{{end}}>
<label for="idHTTPSkipTLSVerify" class="form-check-label">Skip TLS verify</label>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -4,24 +4,23 @@ go 1.18
require (
github.com/hashicorp/go-plugin v1.4.4
github.com/sftpgo/sdk v0.1.0
github.com/sftpgo/sdk v0.1.1
)
require (
github.com/fatih/color v1.13.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/hashicorp/go-hclog v1.2.0 // indirect
github.com/hashicorp/go-hclog v1.2.1 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
google.golang.org/grpc v1.46.2 // indirect
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -20,7 +20,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -51,19 +50,16 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@ -75,13 +71,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/sftpgo/sdk v0.1.0 h1:t94VfxsmNbCYLYRDr3x/UTwSbFFtL9DJ171zkQ3MchQ=
github.com/sftpgo/sdk v0.1.0/go.mod h1:Bhgac6kiwIziILXLzH4wepT8lQXyhF83poDXqZorN6Q=
github.com/sftpgo/sdk v0.1.1 h1:3vGGmRWLr+1vp+Z7OJG2LHt/u9MjTs3odZZtUcbfAsQ=
github.com/sftpgo/sdk v0.1.1/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -98,8 +94,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -108,9 +104,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -121,8 +115,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -142,16 +137,16 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac h1:ByeiW1F67iV9o8ipGskA+HWzSkMbRJuKLlwCdPxzn7A=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -4,23 +4,22 @@ go 1.18
require (
github.com/hashicorp/go-plugin v1.4.4
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4
github.com/sftpgo/sdk v0.1.1
)
require (
github.com/fatih/color v1.13.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/go-hclog v1.2.0 // indirect
github.com/hashicorp/go-hclog v1.2.1 // indirect
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
google.golang.org/grpc v1.46.2 // indirect
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -20,7 +20,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -50,19 +49,16 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@ -74,13 +70,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4 h1:zpu89DMnl3d5Bu3YlvQuu3/KsjkhERgvqgqz+Lnn4CY=
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4/go.mod h1:m5J7DH8unhD5RUsREFRiidP8zgBjup0+iQaxQnYHJOM=
github.com/sftpgo/sdk v0.1.1 h1:3vGGmRWLr+1vp+Z7OJG2LHt/u9MjTs3odZZtUcbfAsQ=
github.com/sftpgo/sdk v0.1.1/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -97,8 +93,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -107,9 +103,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -120,8 +114,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -142,16 +137,16 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac h1:ByeiW1F67iV9o8ipGskA+HWzSkMbRJuKLlwCdPxzn7A=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -2,7 +2,7 @@ package version
import "strings"
const version = "2.3.0-dev"
const version = "2.3.1-dev"
var (
commit = ""

View file

@ -15,6 +15,7 @@ type Filesystem struct {
AzBlobConfig AzBlobFsConfig `json:"azblobconfig,omitempty"`
CryptConfig CryptFsConfig `json:"cryptconfig,omitempty"`
SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"`
HTTPConfig HTTPFsConfig `json:"httpconfig,omitempty"`
}
// SetEmptySecrets sets the secrets to empty
@ -27,6 +28,8 @@ func (f *Filesystem) SetEmptySecrets() {
f.SFTPConfig.Password = kms.NewEmptySecret()
f.SFTPConfig.PrivateKey = kms.NewEmptySecret()
f.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()
f.HTTPConfig.Password = kms.NewEmptySecret()
f.HTTPConfig.APIKey = kms.NewEmptySecret()
}
// SetEmptySecretsIfNil sets the secrets to empty if nil
@ -55,6 +58,12 @@ func (f *Filesystem) SetEmptySecretsIfNil() {
if f.SFTPConfig.KeyPassphrase == nil {
f.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()
}
if f.HTTPConfig.Password == nil {
f.HTTPConfig.Password = kms.NewEmptySecret()
}
if f.HTTPConfig.APIKey == nil {
f.HTTPConfig.APIKey = kms.NewEmptySecret()
}
}
// SetNilSecretsIfEmpty set the secrets to nil if empty.
@ -77,6 +86,7 @@ func (f *Filesystem) SetNilSecretsIfEmpty() {
f.CryptConfig.Passphrase = nil
}
f.SFTPConfig.setNilSecretsIfEmpty()
f.HTTPConfig.setNilSecretsIfEmpty()
}
// IsEqual returns true if the fs is equal to other
@ -95,6 +105,8 @@ func (f *Filesystem) IsEqual(other *Filesystem) bool {
return f.CryptConfig.isEqual(&other.CryptConfig)
case sdk.SFTPFilesystemProvider:
return f.SFTPConfig.isEqual(&other.SFTPConfig)
case sdk.HTTPFilesystemProvider:
return f.HTTPConfig.isEqual(&other.HTTPConfig)
default:
return true
}
@ -112,6 +124,7 @@ func (f *Filesystem) Validate(additionalData string) error {
f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{}
f.HTTPConfig = HTTPFsConfig{}
return nil
case sdk.GCSFilesystemProvider:
if err := f.GCSConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
@ -121,6 +134,7 @@ func (f *Filesystem) Validate(additionalData string) error {
f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{}
f.HTTPConfig = HTTPFsConfig{}
return nil
case sdk.AzureBlobFilesystemProvider:
if err := f.AzBlobConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
@ -130,6 +144,7 @@ func (f *Filesystem) Validate(additionalData string) error {
f.GCSConfig = GCSFsConfig{}
f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{}
f.HTTPConfig = HTTPFsConfig{}
return nil
case sdk.CryptedFilesystemProvider:
if err := f.CryptConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
@ -139,6 +154,7 @@ func (f *Filesystem) Validate(additionalData string) error {
f.GCSConfig = GCSFsConfig{}
f.AzBlobConfig = AzBlobFsConfig{}
f.SFTPConfig = SFTPFsConfig{}
f.HTTPConfig = HTTPFsConfig{}
return nil
case sdk.SFTPFilesystemProvider:
if err := f.SFTPConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
@ -148,6 +164,17 @@ func (f *Filesystem) Validate(additionalData string) error {
f.GCSConfig = GCSFsConfig{}
f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{}
f.HTTPConfig = HTTPFsConfig{}
return nil
case sdk.HTTPFilesystemProvider:
if err := f.HTTPConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
return err
}
f.S3Config = S3FsConfig{}
f.GCSConfig = GCSFsConfig{}
f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{}
return nil
default:
f.Provider = sdk.LocalFilesystemProvider
@ -156,6 +183,7 @@ func (f *Filesystem) Validate(additionalData string) error {
f.AzBlobConfig = AzBlobFsConfig{}
f.CryptConfig = CryptFsConfig{}
f.SFTPConfig = SFTPFsConfig{}
f.HTTPConfig = HTTPFsConfig{}
return nil
}
}
@ -165,24 +193,16 @@ func (f *Filesystem) HasRedactedSecret() bool {
// TODO move vfs specific code into each *FsConfig struct
switch f.Provider {
case sdk.S3FilesystemProvider:
if f.S3Config.AccessSecret.IsRedacted() {
return true
}
return f.S3Config.AccessSecret.IsRedacted()
case sdk.GCSFilesystemProvider:
if f.GCSConfig.Credentials.IsRedacted() {
return true
}
return f.GCSConfig.Credentials.IsRedacted()
case sdk.AzureBlobFilesystemProvider:
if f.AzBlobConfig.AccountKey.IsRedacted() {
return true
}
if f.AzBlobConfig.SASURL.IsRedacted() {
return true
}
return f.AzBlobConfig.SASURL.IsRedacted()
case sdk.CryptedFilesystemProvider:
if f.CryptConfig.Passphrase.IsRedacted() {
return true
}
return f.CryptConfig.Passphrase.IsRedacted()
case sdk.SFTPFilesystemProvider:
if f.SFTPConfig.Password.IsRedacted() {
return true
@ -190,9 +210,12 @@ func (f *Filesystem) HasRedactedSecret() bool {
if f.SFTPConfig.PrivateKey.IsRedacted() {
return true
}
if f.SFTPConfig.KeyPassphrase.IsRedacted() {
return f.SFTPConfig.KeyPassphrase.IsRedacted()
case sdk.HTTPFilesystemProvider:
if f.HTTPConfig.Password.IsRedacted() {
return true
}
return f.HTTPConfig.APIKey.IsRedacted()
}
return false
@ -211,6 +234,8 @@ func (f *Filesystem) HideConfidentialData() {
f.CryptConfig.HideConfidentialData()
case sdk.SFTPFilesystemProvider:
f.SFTPConfig.HideConfidentialData()
case sdk.HTTPFilesystemProvider:
f.HTTPConfig.HideConfidentialData()
}
}
@ -280,6 +305,15 @@ func (f *Filesystem) GetACopy() Filesystem {
PrivateKey: f.SFTPConfig.PrivateKey.Clone(),
KeyPassphrase: f.SFTPConfig.KeyPassphrase.Clone(),
},
HTTPConfig: HTTPFsConfig{
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
Endpoint: f.HTTPConfig.Endpoint,
Username: f.HTTPConfig.Username,
SkipTLSVerify: f.HTTPConfig.SkipTLSVerify,
},
Password: f.HTTPConfig.Password.Clone(),
APIKey: f.HTTPConfig.APIKey.Clone(),
},
}
if len(f.SFTPConfig.Fingerprints) > 0 {
fs.SFTPConfig.Fingerprints = make([]string, len(f.SFTPConfig.Fingerprints))

View file

@ -94,17 +94,19 @@ func (v *BaseVirtualFolder) GetQuotaSummary() string {
func (v *BaseVirtualFolder) GetStorageDescrition() string {
switch v.FsConfig.Provider {
case sdk.LocalFilesystemProvider:
return fmt.Sprintf("Local: %v", v.MappedPath)
return fmt.Sprintf("Local: %s", v.MappedPath)
case sdk.S3FilesystemProvider:
return fmt.Sprintf("S3: %v", v.FsConfig.S3Config.Bucket)
return fmt.Sprintf("S3: %s", v.FsConfig.S3Config.Bucket)
case sdk.GCSFilesystemProvider:
return fmt.Sprintf("GCS: %v", v.FsConfig.GCSConfig.Bucket)
return fmt.Sprintf("GCS: %s", v.FsConfig.GCSConfig.Bucket)
case sdk.AzureBlobFilesystemProvider:
return fmt.Sprintf("AzBlob: %v", v.FsConfig.AzBlobConfig.Container)
return fmt.Sprintf("AzBlob: %s", v.FsConfig.AzBlobConfig.Container)
case sdk.CryptedFilesystemProvider:
return fmt.Sprintf("Encrypted: %v", v.MappedPath)
return fmt.Sprintf("Encrypted: %s", v.MappedPath)
case sdk.SFTPFilesystemProvider:
return fmt.Sprintf("SFTP: %v", v.FsConfig.SFTPConfig.Endpoint)
return fmt.Sprintf("SFTP: %s", v.FsConfig.SFTPConfig.Endpoint)
case sdk.HTTPFilesystemProvider:
return fmt.Sprintf("HTTP: %s", v.FsConfig.HTTPConfig.Endpoint)
default:
return ""
}
@ -128,6 +130,8 @@ func (v *BaseVirtualFolder) hideConfidentialData() {
v.FsConfig.CryptConfig.HideConfidentialData()
case sdk.SFTPFilesystemProvider:
v.FsConfig.SFTPConfig.HideConfidentialData()
case sdk.HTTPFilesystemProvider:
v.FsConfig.HTTPConfig.HideConfidentialData()
}
}
@ -141,38 +145,7 @@ func (v *BaseVirtualFolder) PrepareForRendering() {
// HasRedactedSecret returns true if the folder has a redacted secret
func (v *BaseVirtualFolder) HasRedactedSecret() bool {
switch v.FsConfig.Provider {
case sdk.S3FilesystemProvider:
if v.FsConfig.S3Config.AccessSecret.IsRedacted() {
return true
}
case sdk.GCSFilesystemProvider:
if v.FsConfig.GCSConfig.Credentials.IsRedacted() {
return true
}
case sdk.AzureBlobFilesystemProvider:
if v.FsConfig.AzBlobConfig.AccountKey.IsRedacted() {
return true
}
if v.FsConfig.AzBlobConfig.SASURL.IsRedacted() {
return true
}
case sdk.CryptedFilesystemProvider:
if v.FsConfig.CryptConfig.Passphrase.IsRedacted() {
return true
}
case sdk.SFTPFilesystemProvider:
if v.FsConfig.SFTPConfig.Password.IsRedacted() {
return true
}
if v.FsConfig.SFTPConfig.PrivateKey.IsRedacted() {
return true
}
if v.FsConfig.SFTPConfig.KeyPassphrase.IsRedacted() {
return true
}
}
return false
return v.FsConfig.HasRedactedSecret()
}
// VirtualFolder defines a mapping between an SFTPGo exposed virtual path and a
@ -203,6 +176,8 @@ func (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []
return NewCryptFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.CryptConfig)
case sdk.SFTPFilesystemProvider:
return NewSFTPFs(connectionID, v.VirtualPath, v.MappedPath, forbiddenSelfUsers, v.FsConfig.SFTPConfig)
case sdk.HTTPFilesystemProvider:
return NewHTTPFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.HTTPConfig)
default:
return NewOsFs(connectionID, v.MappedPath, v.VirtualPath), nil
}

712
vfs/httpfs.go Normal file
View file

@ -0,0 +1,712 @@
package vfs
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/eikenb/pipeat"
"github.com/pkg/sftp"
"github.com/sftpgo/sdk"
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/util"
)
const (
// httpFsName is the name for the HTTP Fs implementation
httpFsName = "httpfs"
)
// HTTPFsConfig defines the configuration for HTTP based filesystem
type HTTPFsConfig struct {
sdk.BaseHTTPFsConfig
Password *kms.Secret `json:"password,omitempty"`
APIKey *kms.Secret `json:"api_key,omitempty"`
}
// HideConfidentialData hides confidential data
func (c *HTTPFsConfig) HideConfidentialData() {
if c.Password != nil {
c.Password.Hide()
}
if c.APIKey != nil {
c.APIKey.Hide()
}
}
func (c *HTTPFsConfig) setNilSecretsIfEmpty() {
if c.Password != nil && c.Password.IsEmpty() {
c.Password = nil
}
if c.APIKey != nil && c.APIKey.IsEmpty() {
c.APIKey = nil
}
}
func (c *HTTPFsConfig) setEmptyCredentialsIfNil() {
if c.Password == nil {
c.Password = kms.NewEmptySecret()
}
if c.APIKey == nil {
c.APIKey = kms.NewEmptySecret()
}
}
func (c *HTTPFsConfig) isEqual(other *HTTPFsConfig) bool {
if c.Endpoint != other.Endpoint {
return false
}
if c.Username != other.Username {
return false
}
if c.SkipTLSVerify != other.SkipTLSVerify {
return false
}
c.setEmptyCredentialsIfNil()
other.setEmptyCredentialsIfNil()
if !c.Password.IsEqual(other.Password) {
return false
}
return c.APIKey.IsEqual(other.APIKey)
}
// validate returns an error if the configuration is not valid
func (c *HTTPFsConfig) validate() error {
c.setEmptyCredentialsIfNil()
if c.Endpoint == "" {
return errors.New("httpfs: endpoint cannot be empty")
}
c.Endpoint = strings.TrimRight(c.Endpoint, "/")
_, err := url.Parse(c.Endpoint)
if err != nil {
return fmt.Errorf("httpfs: invalid endpoint: %w", err)
}
if c.Password.IsEncrypted() && !c.Password.IsValid() {
return errors.New("httpfs: invalid encrypted password")
}
if !c.Password.IsEmpty() && !c.Password.IsValidInput() {
return errors.New("httpfs: invalid password")
}
if c.APIKey.IsEncrypted() && !c.APIKey.IsValid() {
return errors.New("httpfs: invalid encrypted API key")
}
if !c.APIKey.IsEmpty() && !c.APIKey.IsValidInput() {
return errors.New("httpfs: invalid API key")
}
return nil
}
// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
func (c *HTTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
if err := c.validate(); err != nil {
return util.NewValidationError(fmt.Sprintf("could not validate HTTP fs config: %v", err))
}
if c.Password.IsPlain() {
c.Password.SetAdditionalData(additionalData)
if err := c.Password.Encrypt(); err != nil {
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs password: %v", err))
}
}
if c.APIKey.IsPlain() {
c.APIKey.SetAdditionalData(additionalData)
if err := c.APIKey.Encrypt(); err != nil {
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs API key: %v", err))
}
}
return nil
}
// HTTPFs is a Fs implementation for the SFTPGo HTTP filesystem backend
type HTTPFs struct {
connectionID string
localTempDir string
// if not empty this fs is mouted as virtual folder in the specified path
mountPath string
config *HTTPFsConfig
client *http.Client
ctxTimeout time.Duration
}
// NewHTTPFs returns an HTTPFs object that allows to interact with SFTPGo HTTP filesystem backends
func NewHTTPFs(connectionID, localTempDir, mountPath string, config HTTPFsConfig) (Fs, error) {
if localTempDir == "" {
if tempPath != "" {
localTempDir = tempPath
} else {
localTempDir = filepath.Clean(os.TempDir())
}
}
if err := config.validate(); err != nil {
return nil, err
}
if !config.Password.IsEmpty() {
if err := config.Password.TryDecrypt(); err != nil {
return nil, err
}
}
if !config.APIKey.IsEmpty() {
if err := config.APIKey.TryDecrypt(); err != nil {
return nil, err
}
}
fs := &HTTPFs{
connectionID: connectionID,
localTempDir: localTempDir,
mountPath: mountPath,
config: &config,
ctxTimeout: 30 * time.Second,
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.MaxResponseHeaderBytes = 1 << 16
transport.WriteBufferSize = 1 << 16
transport.ReadBufferSize = 1 << 16
if config.SkipTLSVerify {
if transport.TLSClientConfig != nil {
transport.TLSClientConfig.InsecureSkipVerify = true
} else {
transport.TLSClientConfig = &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
InsecureSkipVerify: true,
}
}
}
fs.client = &http.Client{
Transport: transport,
}
return fs, nil
}
// Name returns the name for the Fs implementation
func (fs *HTTPFs) Name() string {
return fmt.Sprintf("%v %#v", httpFsName, fs.config.Endpoint)
}
// ConnectionID returns the connection ID associated to this Fs implementation
func (fs *HTTPFs) ConnectionID() string {
return fs.connectionID
}
// Stat returns a FileInfo describing the named file
func (fs *HTTPFs) Stat(name string) (os.FileInfo, error) {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "stat", name, "", "", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var response statResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}
return response.getFileInfo(), nil
}
// Lstat returns a FileInfo describing the named file
func (fs *HTTPFs) Lstat(name string) (os.FileInfo, error) {
return fs.Stat(name)
}
// Open opens the named file for reading
func (fs *HTTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
r, w, err := pipeat.PipeInDir(fs.localTempDir)
if err != nil {
return nil, nil, nil, err
}
ctx, cancelFn := context.WithCancel(context.Background())
var queryString string
if offset > 0 {
queryString = fmt.Sprintf("?offset=%d", offset)
}
go func() {
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "open", name, queryString, "", nil)
if err != nil {
fsLog(fs, logger.LevelError, "download error, path %q, err: %v", name, err)
w.CloseWithError(err) //nolint:errcheck
return
}
defer resp.Body.Close()
n, err := io.Copy(w, resp.Body)
w.CloseWithError(err) //nolint:errcheck
fsLog(fs, logger.LevelDebug, "download completed, path %q size: %v, err: %+v", name, n, err)
}()
return nil, r, cancelFn, nil
}
// Create creates or opens the named file for writing
func (fs *HTTPFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
r, w, err := pipeat.PipeInDir(fs.localTempDir)
if err != nil {
return nil, nil, nil, err
}
p := NewPipeWriter(w)
ctx, cancelFn := context.WithCancel(context.Background())
var queryString string
if flag > 0 {
queryString = fmt.Sprintf("?flags=%d", flag)
}
go func() {
defer cancelFn()
contentType := mime.TypeByExtension(path.Ext(name))
resp, err := fs.sendHTTPRequest(ctx, http.MethodPost, "create", name, queryString, contentType,
&wrapReader{reader: r})
if err != nil {
fsLog(fs, logger.LevelError, "upload error, path %q, err: %v", name, err)
r.CloseWithError(err) //nolint:errcheck
p.Done(err)
return
}
defer resp.Body.Close()
r.CloseWithError(err) //nolint:errcheck
p.Done(err)
fsLog(fs, logger.LevelDebug, "upload completed, path: %q, readed bytes: %d", name, r.GetReadedBytes())
}()
return nil, p, cancelFn, nil
}
// Rename renames (moves) source to target.
func (fs *HTTPFs) Rename(source, target string) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
queryString := fmt.Sprintf("?target=%s", url.QueryEscape(target))
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "rename", source, queryString, "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// Remove removes the named file or (empty) directory.
func (fs *HTTPFs) Remove(name string, isDir bool) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodDelete, "remove", name, "", "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// Mkdir creates a new directory with the specified name and default permissions
func (fs *HTTPFs) Mkdir(name string) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodPost, "mkdir", name, "", "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// Symlink creates source as a symbolic link to target.
func (*HTTPFs) Symlink(source, target string) error {
return ErrVfsUnsupported
}
// Readlink returns the destination of the named symbolic link
func (*HTTPFs) Readlink(name string) (string, error) {
return "", ErrVfsUnsupported
}
// Chown changes the numeric uid and gid of the named file.
func (fs *HTTPFs) Chown(name string, uid int, gid int) error {
/*ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
queryString := fmt.Sprintf("?uid=%d&gid=%d", uid, gid)
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chown", name, queryString, "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil*/
return ErrVfsUnsupported
}
// Chmod changes the mode of the named file to mode.
func (fs *HTTPFs) Chmod(name string, mode os.FileMode) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
queryString := fmt.Sprintf("?mode=%d", mode)
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chmod", name, queryString, "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// Chtimes changes the access and modification times of the named file.
func (fs *HTTPFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
queryString := fmt.Sprintf("?access_time=%s&modification_time=%s", atime.UTC().Format(time.RFC3339),
mtime.UTC().Format(time.RFC3339))
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chtimes", name, queryString, "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// Truncate changes the size of the named file.
// Truncate by path is not supported, while truncating an opened
// file is handled inside base transfer
func (fs *HTTPFs) Truncate(name string, size int64) error {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
queryString := fmt.Sprintf("?size=%d", size)
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "truncate", name, queryString, "", nil)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// ReadDir reads the directory named by dirname and returns
// a list of directory entries.
func (fs *HTTPFs) ReadDir(dirname string) ([]os.FileInfo, error) {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "readdir", dirname, "", "", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var response []statResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}
result := make([]os.FileInfo, 0, len(response))
for _, stat := range response {
result = append(result, stat.getFileInfo())
}
return result, nil
}
// IsUploadResumeSupported returns true if resuming uploads is supported.
func (*HTTPFs) IsUploadResumeSupported() bool {
return false
}
// IsAtomicUploadSupported returns true if atomic upload is supported.
func (*HTTPFs) IsAtomicUploadSupported() bool {
return false
}
// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist
func (*HTTPFs) IsNotExist(err error) bool {
return errors.Is(err, fs.ErrNotExist)
}
// IsPermission returns a boolean indicating whether the error is known to
// report that permission is denied.
func (*HTTPFs) IsPermission(err error) bool {
return errors.Is(err, fs.ErrPermission)
}
// IsNotSupported returns true if the error indicate an unsupported operation
func (*HTTPFs) IsNotSupported(err error) bool {
if err == nil {
return false
}
return err == ErrVfsUnsupported
}
// CheckRootPath creates the specified local root directory if it does not exists
func (fs *HTTPFs) CheckRootPath(username string, uid int, gid int) bool {
// we need a local directory for temporary files
osFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, "")
return osFs.CheckRootPath(username, uid, gid)
}
// ScanRootDirContents returns the number of files and their size
func (fs *HTTPFs) ScanRootDirContents() (int, int64, error) {
return fs.GetDirSize("/")
}
// CheckMetadata checks the metadata consistency
func (*HTTPFs) CheckMetadata() error {
return nil
}
// GetDirSize returns the number of files and the size for a folder
// including any subfolders
func (fs *HTTPFs) GetDirSize(dirname string) (int, int64, error) {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "dirsize", dirname, "", "", nil)
if err != nil {
return 0, 0, err
}
defer resp.Body.Close()
var response dirSizeResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return 0, 0, err
}
return response.Files, response.Size, nil
}
// GetAtomicUploadPath returns the path to use for an atomic upload.
func (*HTTPFs) GetAtomicUploadPath(name string) string {
return ""
}
// GetRelativePath returns the path for a file relative to the user's home dir.
// This is the path as seen by SFTPGo users
func (fs *HTTPFs) GetRelativePath(name string) string {
rel := path.Clean(name)
if rel == "." {
rel = ""
}
if !path.IsAbs(rel) {
rel = "/" + rel
}
if fs.mountPath != "" {
rel = path.Join(fs.mountPath, rel)
}
return rel
}
// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root. The result are unordered
func (fs *HTTPFs) Walk(root string, walkFn filepath.WalkFunc) error {
info, err := fs.Lstat(root)
if err != nil {
return walkFn(root, nil, err)
}
return fs.walk(root, info, walkFn)
}
// Join joins any number of path elements into a single path
func (*HTTPFs) Join(elem ...string) string {
return strings.TrimPrefix(path.Join(elem...), "/")
}
// HasVirtualFolders returns true if folders are emulated
func (*HTTPFs) HasVirtualFolders() bool {
return false
}
// ResolvePath returns the matching filesystem path for the specified virtual path
func (fs *HTTPFs) ResolvePath(virtualPath string) (string, error) {
if fs.mountPath != "" {
virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
}
if !path.IsAbs(virtualPath) {
virtualPath = path.Clean("/" + virtualPath)
}
return virtualPath, nil
}
// GetMimeType returns the content type
func (fs *HTTPFs) GetMimeType(name string) (string, error) {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "stat", name, "", "", nil)
if err != nil {
return "", err
}
defer resp.Body.Close()
var response mimeTypeResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return "", err
}
return response.Mime, nil
}
// Close closes the fs
func (fs *HTTPFs) Close() error {
fs.client.CloseIdleConnections()
return nil
}
// GetAvailableDiskSize returns the available size for the specified path
func (fs *HTTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
defer cancelFn()
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "statvfs", dirName, "", "", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var response statVFSResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return nil, err
}
return response.toSFTPStatVFS(), nil
}
func (fs *HTTPFs) sendHTTPRequest(ctx context.Context, method, base, name, queryString, contentType string,
body io.Reader,
) (*http.Response, error) {
url := fmt.Sprintf("%s/%s/%s%s", fs.config.Endpoint, base, url.PathEscape(name), queryString)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if fs.config.APIKey.GetPayload() != "" {
req.Header.Set("X-API-KEY", fs.config.APIKey.GetPayload())
}
if fs.config.Username != "" || fs.config.Password.GetPayload() != "" {
req.SetBasicAuth(fs.config.Username, fs.config.Password.GetPayload())
}
resp, err := fs.client.Do(req.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("unable to send HTTP request to URL %v: %w", url, err)
}
if err = getErrorFromResponseCode(resp.StatusCode); err != nil {
resp.Body.Close()
return nil, err
}
return resp, nil
}
// walk recursively descends path, calling walkFn.
func (fs *HTTPFs) walk(filePath string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
if !info.IsDir() {
return walkFn(filePath, info, nil)
}
files, err := fs.ReadDir(filePath)
err1 := walkFn(filePath, info, err)
if err != nil || err1 != nil {
return err1
}
for _, fi := range files {
objName := path.Join(filePath, fi.Name())
err = fs.walk(objName, fi, walkFn)
if err != nil {
return err
}
}
return nil
}
func getErrorFromResponseCode(code int) error {
switch code {
case 401, 403:
return os.ErrPermission
case 404:
return os.ErrNotExist
case 501:
return ErrVfsUnsupported
case 200, 201:
return nil
default:
return fmt.Errorf("unexpected response code: %v", code)
}
}
type wrapReader struct {
reader io.Reader
}
func (r *wrapReader) Read(p []byte) (n int, err error) {
return r.reader.Read(p)
}
type statResponse struct {
Name string `json:"name"`
Size int64 `json:"size"`
Mode uint32 `json:"mode"`
LastModified time.Time `json:"last_modified"`
}
func (s *statResponse) getFileInfo() os.FileInfo {
info := NewFileInfo(s.Name, false, s.Size, s.LastModified, false)
info.SetMode(fs.FileMode(s.Mode))
return info
}
type dirSizeResponse struct {
Files int `json:"files"`
Size int64 `json:"size"`
}
type mimeTypeResponse struct {
Mime string `json:"mime"`
}
type statVFSResponse struct {
ID uint32 `json:"-"`
Bsize uint64 `json:"bsize"`
Frsize uint64 `json:"frsize"`
Blocks uint64 `json:"blocks"`
Bfree uint64 `json:"bfree"`
Bavail uint64 `json:"bavail"`
Files uint64 `json:"files"`
Ffree uint64 `json:"ffree"`
Favail uint64 `json:"favail"`
Fsid uint64 `json:"fsid"`
Flag uint64 `json:"flag"`
Namemax uint64 `json:"namemax"`
}
func (s *statVFSResponse) toSFTPStatVFS() *sftp.StatVFS {
return &sftp.StatVFS{
Bsize: s.Bsize,
Frsize: s.Frsize,
Blocks: s.Blocks,
Bfree: s.Bfree,
Bavail: s.Bavail,
Files: s.Files,
Ffree: s.Ffree,
Favail: s.Ffree,
Flag: s.Flag,
Namemax: s.Namemax,
}
}

View file

@ -288,7 +288,7 @@ func (*OsFs) Join(elem ...string) string {
// ResolvePath returns the matching filesystem path for the specified sftp path
func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
if !filepath.IsAbs(fs.rootDir) {
return "", fmt.Errorf("invalid root path: %v", fs.rootDir)
return "", fmt.Errorf("invalid root path %q", fs.rootDir)
}
if fs.mountPath != "" {
virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)

View file

@ -168,7 +168,7 @@ func (c *SFTPFsConfig) validateCredentials() error {
return nil
}
// ValidateAndEncryptCredentials encrypts password and/or private key if they are in plain text
// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
func (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
if err := c.validate(); err != nil {
return util.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err))

View file

@ -669,6 +669,11 @@ func IsSFTPFs(fs Fs) bool {
return strings.HasPrefix(fs.Name(), sftpFsName)
}
// IsHTTPFs returns true if fs is an HTTP filesystem
func IsHTTPFs(fs Fs) bool {
return strings.HasPrefix(fs.Name(), httpFsName)
}
// IsBufferedSFTPFs returns true if this is a buffered SFTP filesystem
func IsBufferedSFTPFs(fs Fs) bool {
if !IsSFTPFs(fs) {
@ -693,6 +698,11 @@ func IsLocalOrSFTPFs(fs Fs) bool {
return IsLocalOsFs(fs) || IsSFTPFs(fs)
}
// HasTruncateSupport returns true if the fs supports truncate files
func HasTruncateSupport(fs Fs) bool {
return IsLocalOsFs(fs) || IsSFTPFs(fs) || IsHTTPFs(fs)
}
// HasOpenRWSupport returns true if the fs can open a file
// for reading and writing at the same time
func HasOpenRWSupport(fs Fs) bool {

View file

@ -254,7 +254,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
}
initialSize := int64(0)
truncatedSize := int64(0) // bytes truncated and not included in quota
if vfs.IsLocalOrSFTPFs(fs) {
if vfs.HasTruncateSupport(fs) {
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck