Compare commits
21 commits
Author | SHA1 | Date | |
---|---|---|---|
|
ca464b5ffc | ||
|
27b5f1bf59 | ||
|
ae9c540640 | ||
|
ad8ebcc031 | ||
|
c69c27f586 | ||
|
7d20c6b50d | ||
|
9ca136370e | ||
|
29836edf2b | ||
|
0ad6f031e8 | ||
|
a9838d2e6d | ||
|
7a9272ddfc | ||
|
69c3a85ea5 | ||
|
d03020e2b8 | ||
|
89bc50aa81 | ||
|
d27c32e06b | ||
|
53eca2c2bb | ||
|
0e9351e4ad | ||
|
43f468547f | ||
|
c372ca4136 | ||
|
2b328c82bb | ||
|
6321d51a24 |
70 changed files with 840 additions and 1728 deletions
2
.github/workflows/development.yml
vendored
2
.github/workflows/development.yml
vendored
|
@ -2,7 +2,7 @@ name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [2.1.x]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -4,8 +4,6 @@ on:
|
||||||
#schedule:
|
#schedule:
|
||||||
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
|
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
||||||
tags: 'v*'
|
tags: 'v*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.16.5
|
GO_VERSION: 1.16.8
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare-sources-with-deps:
|
prepare-sources-with-deps:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !noportable
|
||||||
// +build !noportable
|
// +build !noportable
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build noportable
|
||||||
// +build noportable
|
// +build noportable
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
|
@ -71,6 +71,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.CompletionOptions.DisableDefaultCmd = true
|
||||||
rootCmd.Flags().BoolP("version", "v", false, "")
|
rootCmd.Flags().BoolP("version", "v", false, "")
|
||||||
rootCmd.Version = version.GetAsString()
|
rootCmd.Version = version.GetAsString()
|
||||||
rootCmd.SetVersionTemplate(`{{printf "SFTPGo "}}{{printf "%s" .Version}}
|
rootCmd.SetVersionTemplate(`{{printf "SFTPGo "}}{{printf "%s" .Version}}
|
||||||
|
|
|
@ -409,9 +409,8 @@ func (c *Configuration) IsAtomicUploadEnabled() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxyListener returns a wrapper for the given listener that supports the
|
// GetProxyListener returns a wrapper for the given listener that supports the
|
||||||
// HAProxy Proxy Protocol or nil if the proxy protocol is not configured
|
// HAProxy Proxy Protocol
|
||||||
func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Listener, error) {
|
func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Listener, error) {
|
||||||
var proxyListener *proxyproto.Listener
|
|
||||||
var err error
|
var err error
|
||||||
if c.ProxyProtocol > 0 {
|
if c.ProxyProtocol > 0 {
|
||||||
var policyFunc func(upstream net.Addr) (proxyproto.Policy, error)
|
var policyFunc func(upstream net.Addr) (proxyproto.Policy, error)
|
||||||
|
@ -433,12 +432,13 @@ func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Lis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxyListener = &proxyproto.Listener{
|
return &proxyproto.Listener{
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
Policy: policyFunc,
|
Policy: policyFunc,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
return nil, errors.New("proxy protocol not configured")
|
||||||
return proxyListener, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteStartupHook runs the startup hook if defined
|
// ExecuteStartupHook runs the startup hook if defined
|
||||||
|
|
|
@ -730,6 +730,26 @@ func TestParseAllowedIPAndRanges(t *testing.T) {
|
||||||
assert.False(t, allow[1](net.ParseIP("172.16.1.1")))
|
assert.False(t, allow[1](net.ParseIP("172.16.1.1")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHideConfidentialData(t *testing.T) {
|
||||||
|
for _, provider := range []vfs.FilesystemProvider{vfs.S3FilesystemProvider, vfs.GCSFilesystemProvider,
|
||||||
|
vfs.AzureBlobFilesystemProvider, vfs.CryptedFilesystemProvider, vfs.SFTPFilesystemProvider} {
|
||||||
|
u := dataprovider.User{
|
||||||
|
FsConfig: vfs.Filesystem{
|
||||||
|
Provider: provider,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
u.PrepareForRendering()
|
||||||
|
f := vfs.BaseVirtualFolder{
|
||||||
|
FsConfig: vfs.Filesystem{
|
||||||
|
Provider: provider,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
f.PrepareForRendering()
|
||||||
|
}
|
||||||
|
a := dataprovider.Admin{}
|
||||||
|
a.HideConfidentialData()
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkBcryptHashing(b *testing.B) {
|
func BenchmarkBcryptHashing(b *testing.B) {
|
||||||
bcryptPassword := "bcryptpassword"
|
bcryptPassword := "bcryptpassword"
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
|
|
|
@ -21,8 +21,11 @@ import (
|
||||||
// BaseConnection defines common fields for a connection using any supported protocol
|
// BaseConnection defines common fields for a connection using any supported protocol
|
||||||
type BaseConnection struct {
|
type BaseConnection struct {
|
||||||
// last activity for this connection.
|
// last activity for this connection.
|
||||||
// Since this is accessed atomically we put as first element of the struct achieve 64 bit alignment
|
// Since this field is accessed atomically we put it as first element of the struct to achieve 64 bit alignment
|
||||||
lastActivity int64
|
lastActivity int64
|
||||||
|
// unique ID for a transfer.
|
||||||
|
// This field is accessed atomically so we put it at the beginning of the struct to achieve 64 bit alignment
|
||||||
|
transferID uint64
|
||||||
// Unique identifier for the connection
|
// Unique identifier for the connection
|
||||||
ID string
|
ID string
|
||||||
// user associated with this connection if any
|
// user associated with this connection if any
|
||||||
|
@ -32,7 +35,6 @@ type BaseConnection struct {
|
||||||
protocol string
|
protocol string
|
||||||
remoteAddr string
|
remoteAddr string
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
transferID uint64
|
|
||||||
activeTransfers []ActiveTransfer
|
activeTransfers []ActiveTransfer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -236,17 +236,27 @@ func (d *memoryDefender) GetHosts() []*DefenderEntry {
|
||||||
|
|
||||||
var result []*DefenderEntry
|
var result []*DefenderEntry
|
||||||
for k, v := range d.banned {
|
for k, v := range d.banned {
|
||||||
|
if v.After(time.Now()) {
|
||||||
result = append(result, &DefenderEntry{
|
result = append(result, &DefenderEntry{
|
||||||
IP: k,
|
IP: k,
|
||||||
BanTime: v,
|
BanTime: v,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for k, v := range d.hosts {
|
for k, v := range d.hosts {
|
||||||
|
score := 0
|
||||||
|
for _, event := range v.Events {
|
||||||
|
if event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {
|
||||||
|
score += event.score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if score > 0 {
|
||||||
result = append(result, &DefenderEntry{
|
result = append(result, &DefenderEntry{
|
||||||
IP: k,
|
IP: k,
|
||||||
Score: v.TotalScore,
|
Score: score,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -257,18 +267,28 @@ func (d *memoryDefender) GetHost(ip string) (*DefenderEntry, error) {
|
||||||
defer d.RUnlock()
|
defer d.RUnlock()
|
||||||
|
|
||||||
if banTime, ok := d.banned[ip]; ok {
|
if banTime, ok := d.banned[ip]; ok {
|
||||||
|
if banTime.After(time.Now()) {
|
||||||
return &DefenderEntry{
|
return &DefenderEntry{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
BanTime: banTime,
|
BanTime: banTime,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ev, ok := d.hosts[ip]; ok {
|
if hs, ok := d.hosts[ip]; ok {
|
||||||
|
score := 0
|
||||||
|
for _, event := range hs.Events {
|
||||||
|
if event.dateTime.Add(time.Duration(d.config.ObservationTime) * time.Minute).After(time.Now()) {
|
||||||
|
score += event.score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if score > 0 {
|
||||||
return &DefenderEntry{
|
return &DefenderEntry{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
Score: ev.TotalScore,
|
Score: score,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil, dataprovider.NewRecordNotFoundError("host not found")
|
return nil, dataprovider.NewRecordNotFoundError("host not found")
|
||||||
}
|
}
|
||||||
|
@ -339,9 +359,12 @@ func (d *memoryDefender) AddEvent(ip string, event HostEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore events for already banned hosts
|
// ignore events for already banned hosts
|
||||||
if _, ok := d.banned[ip]; ok {
|
if v, ok := d.banned[ip]; ok {
|
||||||
|
if v.After(time.Now()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
delete(d.banned, ip)
|
||||||
|
}
|
||||||
|
|
||||||
var score int
|
var score int
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,77 @@ func TestBasicDefender(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpiredHostBans(t *testing.T) {
|
||||||
|
config := &DefenderConfig{
|
||||||
|
Enabled: true,
|
||||||
|
BanTime: 10,
|
||||||
|
BanTimeIncrement: 2,
|
||||||
|
Threshold: 5,
|
||||||
|
ScoreInvalid: 2,
|
||||||
|
ScoreValid: 1,
|
||||||
|
ScoreLimitExceeded: 3,
|
||||||
|
ObservationTime: 15,
|
||||||
|
EntriesSoftLimit: 1,
|
||||||
|
EntriesHardLimit: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := newInMemoryDefender(config)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
defender := d.(*memoryDefender)
|
||||||
|
|
||||||
|
testIP := "1.2.3.4"
|
||||||
|
defender.banned[testIP] = time.Now().Add(-24 * time.Hour)
|
||||||
|
|
||||||
|
// the ban is expired testIP should not be listed
|
||||||
|
res := defender.GetHosts()
|
||||||
|
assert.Len(t, res, 0)
|
||||||
|
|
||||||
|
assert.False(t, defender.IsBanned(testIP))
|
||||||
|
_, err = defender.GetHost(testIP)
|
||||||
|
assert.Error(t, err)
|
||||||
|
_, ok := defender.banned[testIP]
|
||||||
|
assert.True(t, ok)
|
||||||
|
// now add an event for an expired banned ip, it should be removed
|
||||||
|
defender.AddEvent(testIP, HostEventLoginFailed)
|
||||||
|
assert.False(t, defender.IsBanned(testIP))
|
||||||
|
entry, err := defender.GetHost(testIP)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testIP, entry.IP)
|
||||||
|
assert.Empty(t, entry.GetBanTime())
|
||||||
|
assert.Equal(t, 1, entry.Score)
|
||||||
|
|
||||||
|
res = defender.GetHosts()
|
||||||
|
if assert.Len(t, res, 1) {
|
||||||
|
assert.Equal(t, testIP, res[0].IP)
|
||||||
|
assert.Empty(t, res[0].GetBanTime())
|
||||||
|
assert.Equal(t, 1, res[0].Score)
|
||||||
|
}
|
||||||
|
|
||||||
|
events := []hostEvent{
|
||||||
|
{
|
||||||
|
dateTime: time.Now().Add(-24 * time.Hour),
|
||||||
|
score: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dateTime: time.Now().Add(-24 * time.Hour),
|
||||||
|
score: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hs := hostScore{
|
||||||
|
Events: events,
|
||||||
|
TotalScore: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
defender.hosts[testIP] = hs
|
||||||
|
// the recorded scored are too old
|
||||||
|
res = defender.GetHosts()
|
||||||
|
assert.Len(t, res, 0)
|
||||||
|
_, err = defender.GetHost(testIP)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadHostListFromFile(t *testing.T) {
|
func TestLoadHostListFromFile(t *testing.T) {
|
||||||
_, err := loadHostListFromFile(".")
|
_, err := loadHostListFromFile(".")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
|
@ -147,7 +147,7 @@ func Init() {
|
||||||
MACs: []string{},
|
MACs: []string{},
|
||||||
TrustedUserCAKeys: []string{},
|
TrustedUserCAKeys: []string{},
|
||||||
LoginBannerFile: "",
|
LoginBannerFile: "",
|
||||||
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
|
EnabledSSHCommands: []string{},
|
||||||
KeyboardInteractiveHook: "",
|
KeyboardInteractiveHook: "",
|
||||||
PasswordAuthentication: true,
|
PasswordAuthentication: true,
|
||||||
},
|
},
|
||||||
|
@ -955,7 +955,7 @@ func setViperDefaults() {
|
||||||
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
|
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
|
||||||
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
|
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
|
||||||
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
|
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
|
||||||
viper.SetDefault("sftpd.enabled_ssh_commands", globalConf.SFTPD.EnabledSSHCommands)
|
viper.SetDefault("sftpd.enabled_ssh_commands", sftpd.GetDefaultSSHCommands())
|
||||||
viper.SetDefault("sftpd.keyboard_interactive_auth_hook", globalConf.SFTPD.KeyboardInteractiveHook)
|
viper.SetDefault("sftpd.keyboard_interactive_auth_hook", globalConf.SFTPD.KeyboardInteractiveHook)
|
||||||
viper.SetDefault("sftpd.password_authentication", globalConf.SFTPD.PasswordAuthentication)
|
viper.SetDefault("sftpd.password_authentication", globalConf.SFTPD.PasswordAuthentication)
|
||||||
viper.SetDefault("ftpd.banner", globalConf.FTPD.Banner)
|
viper.SetDefault("ftpd.banner", globalConf.FTPD.Banner)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !linux
|
||||||
// +build !linux
|
// +build !linux
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
|
@ -102,6 +102,35 @@ func TestEmptyBanner(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnabledSSHCommands(t *testing.T) {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
configDir := ".."
|
||||||
|
confName := tempConfigName + ".json"
|
||||||
|
configFilePath := filepath.Join(configDir, confName)
|
||||||
|
err := config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reset()
|
||||||
|
|
||||||
|
sftpdConf := config.GetSFTPDConfig()
|
||||||
|
sftpdConf.EnabledSSHCommands = []string{"scp"}
|
||||||
|
c := make(map[string]sftpd.Configuration)
|
||||||
|
c["sftpd"] = sftpdConf
|
||||||
|
jsonConf, err := json.Marshal(c)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(configFilePath, jsonConf, os.ModePerm)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = config.LoadConfig(configDir, confName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
sftpdConf = config.GetSFTPDConfig()
|
||||||
|
if assert.Len(t, sftpdConf.EnabledSSHCommands, 1) {
|
||||||
|
assert.Equal(t, "scp", sftpdConf.EnabledSSHCommands[0])
|
||||||
|
}
|
||||||
|
err = os.Remove(configFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestInvalidUploadMode(t *testing.T) {
|
func TestInvalidUploadMode(t *testing.T) {
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nobolt
|
||||||
// +build !nobolt
|
// +build !nobolt
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nobolt
|
||||||
// +build nobolt
|
// +build nobolt
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1560,6 +1560,7 @@ func createUserPasswordHash(user *User) error {
|
||||||
// ValidateFolder returns an error if the folder is not valid
|
// ValidateFolder returns an error if the folder is not valid
|
||||||
// FIXME: this should be defined as Folder struct method
|
// FIXME: this should be defined as Folder struct method
|
||||||
func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||||
|
folder.FsConfig.SetEmptySecretsIfNil()
|
||||||
if folder.Name == "" {
|
if folder.Name == "" {
|
||||||
return &ValidationError{err: "folder name is mandatory"}
|
return &ValidationError{err: "folder name is mandatory"}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nomysql
|
||||||
// +build !nomysql
|
// +build !nomysql
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nomysql
|
||||||
// +build nomysql
|
// +build nomysql
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nopgsql
|
||||||
// +build !nopgsql
|
// +build !nopgsql
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nopgsql
|
||||||
// +build nopgsql
|
// +build nopgsql
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nosqlite
|
||||||
// +build !nosqlite
|
// +build !nosqlite
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nosqlite
|
||||||
// +build nosqlite
|
// +build nosqlite
|
||||||
|
|
||||||
package dataprovider
|
package dataprovider
|
||||||
|
|
|
@ -344,18 +344,32 @@ func (u *User) hideConfidentialData() {
|
||||||
u.Password = ""
|
u.Password = ""
|
||||||
switch u.FsConfig.Provider {
|
switch u.FsConfig.Provider {
|
||||||
case vfs.S3FilesystemProvider:
|
case vfs.S3FilesystemProvider:
|
||||||
|
if u.FsConfig.S3Config.AccessSecret != nil {
|
||||||
u.FsConfig.S3Config.AccessSecret.Hide()
|
u.FsConfig.S3Config.AccessSecret.Hide()
|
||||||
|
}
|
||||||
case vfs.GCSFilesystemProvider:
|
case vfs.GCSFilesystemProvider:
|
||||||
|
if u.FsConfig.GCSConfig.Credentials != nil {
|
||||||
u.FsConfig.GCSConfig.Credentials.Hide()
|
u.FsConfig.GCSConfig.Credentials.Hide()
|
||||||
|
}
|
||||||
case vfs.AzureBlobFilesystemProvider:
|
case vfs.AzureBlobFilesystemProvider:
|
||||||
|
if u.FsConfig.AzBlobConfig.AccountKey != nil {
|
||||||
u.FsConfig.AzBlobConfig.AccountKey.Hide()
|
u.FsConfig.AzBlobConfig.AccountKey.Hide()
|
||||||
|
}
|
||||||
|
if u.FsConfig.AzBlobConfig.SASURL != nil {
|
||||||
u.FsConfig.AzBlobConfig.SASURL.Hide()
|
u.FsConfig.AzBlobConfig.SASURL.Hide()
|
||||||
|
}
|
||||||
case vfs.CryptedFilesystemProvider:
|
case vfs.CryptedFilesystemProvider:
|
||||||
|
if u.FsConfig.CryptConfig.Passphrase != nil {
|
||||||
u.FsConfig.CryptConfig.Passphrase.Hide()
|
u.FsConfig.CryptConfig.Passphrase.Hide()
|
||||||
|
}
|
||||||
case vfs.SFTPFilesystemProvider:
|
case vfs.SFTPFilesystemProvider:
|
||||||
|
if u.FsConfig.SFTPConfig.Password != nil {
|
||||||
u.FsConfig.SFTPConfig.Password.Hide()
|
u.FsConfig.SFTPConfig.Password.Hide()
|
||||||
|
}
|
||||||
|
if u.FsConfig.SFTPConfig.PrivateKey != nil {
|
||||||
u.FsConfig.SFTPConfig.PrivateKey.Hide()
|
u.FsConfig.SFTPConfig.PrivateKey.Hide()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubDirPermissions returns permissions for sub directories
|
// GetSubDirPermissions returns permissions for sub directories
|
||||||
|
|
|
@ -4,10 +4,10 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
|
||||||
|
|
||||||
## Supported tags and respective Dockerfile links
|
## Supported tags and respective Dockerfile links
|
||||||
|
|
||||||
- [v2.1.0, v2.1, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.1.0/Dockerfile)
|
- [v2.1.2, v2.1, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.1.2/Dockerfile)
|
||||||
- [v2.1.0-alpine, v2.1-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.1.0/Dockerfile.alpine)
|
- [v2.1.2-alpine, v2.1-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.1.2/Dockerfile.alpine)
|
||||||
- [v2.1.0-slim, v2.1-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.1.0/Dockerfile)
|
- [v2.1.2-slim, v2.1-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.1.2/Dockerfile)
|
||||||
- [v2.1.0-alpine-slim, v2.1-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.1.0/Dockerfile.alpine)
|
- [v2.1.2-alpine-slim, v2.1-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.1.2/Dockerfile.alpine)
|
||||||
- [edge](../Dockerfile)
|
- [edge](../Dockerfile)
|
||||||
- [edge-alpine](../Dockerfile.alpine)
|
- [edge-alpine](../Dockerfile.alpine)
|
||||||
- [edge-slim](../Dockerfile)
|
- [edge-slim](../Dockerfile)
|
||||||
|
|
|
@ -2275,13 +2275,13 @@ func TestActiveModeDisabled(t *testing.T) {
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
code, response, err := client.SendCustomCommand("PORT 10,2,0,2,4,31")
|
code, response, err := client.SendCustomCommand("PORT 10,2,0,2,4,31")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, ftp.StatusCommandOK, code)
|
assert.Equal(t, ftp.StatusBadArguments, code)
|
||||||
assert.Equal(t, "PORT command successful", response)
|
assert.Equal(t, "Your request does not meet the configured security requirements", response)
|
||||||
|
|
||||||
code, response, err = client.SendCustomCommand("EPRT |1|132.235.1.2|6275|")
|
code, response, err = client.SendCustomCommand("EPRT |1|132.235.1.2|6275|")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, ftp.StatusCommandOK, code)
|
assert.Equal(t, ftp.StatusBadArguments, code)
|
||||||
assert.Equal(t, "EPRT command successful", response)
|
assert.Equal(t, "Your request does not meet the configured security requirements", response)
|
||||||
|
|
||||||
err = client.Quit()
|
err = client.Quit()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/eikenb/pipeat"
|
"github.com/eikenb/pipeat"
|
||||||
|
ftpserver "github.com/fclairamb/ftpserverlib"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
@ -250,6 +251,7 @@ xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockFTPClientContext struct {
|
type mockFTPClientContext struct {
|
||||||
|
lastDataChannel ftpserver.DataChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cc mockFTPClientContext) Path() string {
|
func (cc mockFTPClientContext) Path() string {
|
||||||
|
@ -294,6 +296,10 @@ func (cc mockFTPClientContext) GetLastCommand() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cc mockFTPClientContext) GetLastDataChannel() ftpserver.DataChannel {
|
||||||
|
return cc.lastDataChannel
|
||||||
|
}
|
||||||
|
|
||||||
// MockOsFs mockable OsFs
|
// MockOsFs mockable OsFs
|
||||||
type MockOsFs struct {
|
type MockOsFs struct {
|
||||||
vfs.Fs
|
vfs.Fs
|
||||||
|
|
|
@ -94,7 +94,7 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ftpListener net.Listener
|
var ftpListener net.Listener
|
||||||
if common.Config.ProxyProtocol > 0 && s.binding.ApplyProxyConfig {
|
if s.binding.HasProxy() {
|
||||||
listener, err := net.Listen("tcp", s.binding.GetAddress())
|
listener, err := net.Listen("tcp", s.binding.GetAddress())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(logSender, "", "error starting listener on address %v: %v", s.binding.GetAddress(), err)
|
logger.Warn(logSender, "", "error starting listener on address %v: %v", s.binding.GetAddress(), err)
|
||||||
|
@ -105,6 +105,9 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
|
||||||
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
|
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if s.binding.TLSMode == 2 && s.tlsConfig != nil {
|
||||||
|
ftpListener = tls.NewListener(ftpListener, s.tlsConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.binding.TLSMode < 0 || s.binding.TLSMode > 2 {
|
if s.binding.TLSMode < 0 || s.binding.TLSMode > 2 {
|
||||||
|
@ -197,6 +200,14 @@ func (s *Server) AuthUser(cc ftpserver.ClientContext, username, password string)
|
||||||
return connection, nil
|
return connection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapPassiveListener implements the MainDriverExtensionPassiveWrapper interface
|
||||||
|
func (s *Server) WrapPassiveListener(listener net.Listener) (net.Listener, error) {
|
||||||
|
if s.binding.HasProxy() {
|
||||||
|
return common.Config.GetProxyListener(listener)
|
||||||
|
}
|
||||||
|
return listener, nil
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyConnection checks whether a user should be authenticated using a client certificate without prompting for a password
|
// VerifyConnection checks whether a user should be authenticated using a client certificate without prompting for a password
|
||||||
func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsConn *tls.Conn) (ftpserver.ClientDriver, error) {
|
func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsConn *tls.Conn) (ftpserver.ClientDriver, error) {
|
||||||
if !s.binding.isMutualTLSEnabled() {
|
if !s.binding.isMutualTLSEnabled() {
|
||||||
|
|
73
go.mod
73
go.mod
|
@ -3,71 +3,62 @@ module github.com/drakkan/sftpgo
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/storage v1.15.0
|
cloud.google.com/go/storage v1.16.1
|
||||||
github.com/Azure/azure-storage-blob-go v0.13.0
|
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
|
|
||||||
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
|
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
|
||||||
github.com/aws/aws-sdk-go v1.38.61
|
github.com/aws/aws-sdk-go v1.40.41
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
github.com/cockroachdb/cockroach-go/v2 v2.1.1
|
||||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||||
github.com/fclairamb/ftpserverlib v0.13.3-0.20210614220040-27dccea41813
|
github.com/fclairamb/ftpserverlib v0.15.1-0.20210910204600-c38788485016
|
||||||
github.com/frankban/quicktest v1.13.0 // indirect
|
github.com/frankban/quicktest v1.13.1 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.0.3
|
github.com/go-chi/chi/v5 v5.0.4
|
||||||
github.com/go-chi/jwtauth/v5 v5.0.1
|
github.com/go-chi/jwtauth/v5 v5.0.1
|
||||||
github.com/go-chi/render v1.0.1
|
github.com/go-chi/render v1.0.1
|
||||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/goccy/go-json v0.7.0 // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0
|
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||||
github.com/klauspost/compress v1.13.1
|
github.com/klauspost/compress v1.13.5
|
||||||
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
github.com/lestrrat-go/jwx v1.2.6
|
||||||
github.com/lestrrat-go/jwx v1.2.1
|
github.com/lib/pq v1.10.3
|
||||||
github.com/lib/pq v1.10.2
|
github.com/mattn/go-sqlite3 v1.14.8
|
||||||
github.com/magiconair/properties v1.8.5 // indirect
|
github.com/miekg/dns v1.1.43 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.7
|
|
||||||
github.com/miekg/dns v1.1.42 // indirect
|
|
||||||
github.com/minio/sio v0.3.0
|
github.com/minio/sio v0.3.0
|
||||||
github.com/otiai10/copy v1.6.0
|
github.com/otiai10/copy v1.6.0
|
||||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
github.com/pires/go-proxyproto v0.6.1
|
||||||
github.com/pires/go-proxyproto v0.5.0
|
github.com/pkg/sftp v1.13.3
|
||||||
github.com/pkg/sftp v1.13.1
|
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.11.0
|
||||||
github.com/prometheus/common v0.29.0 // indirect
|
github.com/prometheus/common v0.30.0 // indirect
|
||||||
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b
|
github.com/rs/cors v1.8.0
|
||||||
github.com/rs/xid v1.3.0
|
github.com/rs/xid v1.3.0
|
||||||
github.com/rs/zerolog v1.23.0
|
github.com/rs/zerolog v1.25.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.21.5
|
github.com/shirou/gopsutil/v3 v3.21.8
|
||||||
github.com/spf13/afero v1.6.0
|
github.com/spf13/afero v1.6.0
|
||||||
github.com/spf13/cast v1.3.1 // indirect
|
github.com/spf13/cobra v1.2.1
|
||||||
github.com/spf13/cobra v1.1.3
|
github.com/spf13/viper v1.8.1
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
|
||||||
github.com/spf13/viper v1.7.1
|
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20210427212133-86f8378cf140
|
github.com/studio-b12/gowebdav v0.0.0-20210630100626-7ff61aa87be8
|
||||||
github.com/yl2chen/cidranger v1.0.2
|
github.com/yl2chen/cidranger v1.0.2
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
go.uber.org/automaxprocs v1.4.0
|
go.uber.org/automaxprocs v1.4.0
|
||||||
gocloud.dev v0.23.0
|
gocloud.dev v0.24.0
|
||||||
gocloud.dev/secrets/hashivault v0.23.0
|
gocloud.dev/secrets/hashivault v0.24.0
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
golang.org/x/net v0.0.0-20210907225631-ff17edfbf26d
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
|
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0
|
||||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
|
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
|
||||||
google.golang.org/api v0.48.0
|
google.golang.org/api v0.56.0
|
||||||
google.golang.org/genproto v0.0.0-20210614182748-5b3b54cad159 // indirect
|
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
github.com/eikenb/pipeat => github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639
|
||||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20210515063737-edf1d3b63536
|
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20210908103413-a132997f748f
|
||||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20210615043241-a7f9e02422df
|
golang.org/x/net => github.com/drakkan/net v0.0.0-20210908102438-2debf45fec0b
|
||||||
)
|
)
|
||||||
|
|
|
@ -190,7 +190,10 @@ func Get(url string) (*http.Response, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
addHeaders(req, url)
|
addHeaders(req, url)
|
||||||
return GetHTTPClient().Do(req)
|
client := GetHTTPClient()
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
|
||||||
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post issues a POST to the specified URL
|
// Post issues a POST to the specified URL
|
||||||
|
@ -201,7 +204,10 @@ func Post(url string, contentType string, body io.Reader) (*http.Response, error
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
addHeaders(req, url)
|
addHeaders(req, url)
|
||||||
return GetHTTPClient().Do(req)
|
client := GetHTTPClient()
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
|
||||||
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetryableGet issues a GET to the specified URL using the retryable client
|
// RetryableGet issues a GET to the specified URL using the retryable client
|
||||||
|
@ -211,7 +217,10 @@ func RetryableGet(url string) (*http.Response, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
addHeadersToRetryableReq(req, url)
|
addHeadersToRetryableReq(req, url)
|
||||||
return GetRetraybleHTTPClient().Do(req)
|
client := GetRetraybleHTTPClient()
|
||||||
|
defer client.HTTPClient.CloseIdleConnections()
|
||||||
|
|
||||||
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetryablePost issues a POST to the specified URL using the retryable client
|
// RetryablePost issues a POST to the specified URL using the retryable client
|
||||||
|
@ -222,7 +231,10 @@ func RetryablePost(url string, contentType string, body io.Reader) (*http.Respon
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
addHeadersToRetryableReq(req, url)
|
addHeadersToRetryableReq(req, url)
|
||||||
return GetRetraybleHTTPClient().Do(req)
|
client := GetRetraybleHTTPClient()
|
||||||
|
defer client.HTTPClient.CloseIdleConnections()
|
||||||
|
|
||||||
|
return client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addHeaders(req *http.Request, url string) {
|
func addHeaders(req *http.Request, url string) {
|
||||||
|
|
|
@ -363,7 +363,7 @@ func parseRangeRequest(bytesRange string, size int64) (int64, int64, error) {
|
||||||
|
|
||||||
func updateLoginMetrics(user *dataprovider.User, ip string, err error) {
|
func updateLoginMetrics(user *dataprovider.User, ip string, err error) {
|
||||||
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
||||||
if err != nil && err != common.ErrInternalFailure {
|
if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||||
logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolHTTP, err.Error())
|
logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolHTTP, err.Error())
|
||||||
event := common.HostEventLoginFailed
|
event := common.HostEventLoginFailed
|
||||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||||
|
|
|
@ -5944,6 +5944,11 @@ func TestWebAdminSetupMock(t *testing.T) {
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusFound, rr)
|
checkResponseCode(t, http.StatusFound, rr)
|
||||||
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
||||||
|
req, err = http.NewRequest(http.MethodGet, webClientLoginPath, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusFound, rr)
|
||||||
|
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
|
||||||
|
|
||||||
csrfToken, err := getCSRFToken(httpBaseURL + webAdminSetupPath)
|
csrfToken, err := getCSRFToken(httpBaseURL + webAdminSetupPath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -17,7 +17,7 @@ info:
|
||||||
Several storage backends are supported and they are configurable per user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.
|
Several storage backends are supported and they are configurable per user, so you can serve a local directory for a user and an S3 bucket (or part of it) for another one.
|
||||||
SFTPGo also supports virtual folders, a virtual folder can use any of the supported storage backends. So you can have, for example, an S3 user that exposes a GCS bucket (or part of it) on a specified path and an encrypted local filesystem on another one.
|
SFTPGo also supports virtual folders, a virtual folder can use any of the supported storage backends. So you can have, for example, an S3 user that exposes a GCS bucket (or part of it) on a specified path and an encrypted local filesystem on another one.
|
||||||
Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.
|
Virtual folders can be private or shared among multiple users, for shared virtual folders you can define different quota limits for each user.
|
||||||
version: 2.1.0
|
version: 2.1.2
|
||||||
contact:
|
contact:
|
||||||
name: API support
|
name: API support
|
||||||
url: 'https://github.com/drakkan/sftpgo'
|
url: 'https://github.com/drakkan/sftpgo'
|
||||||
|
|
|
@ -530,9 +530,9 @@ func (s *httpdServer) initializeRouter() {
|
||||||
s.router = chi.NewRouter()
|
s.router = chi.NewRouter()
|
||||||
|
|
||||||
s.router.Use(middleware.RequestID)
|
s.router.Use(middleware.RequestID)
|
||||||
|
s.router.Use(s.checkConnection)
|
||||||
s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
|
||||||
s.router.Use(recoverer)
|
s.router.Use(recoverer)
|
||||||
s.router.Use(s.checkConnection)
|
|
||||||
s.router.Use(middleware.GetHead)
|
s.router.Use(middleware.GetHead)
|
||||||
s.router.Use(middleware.StripSlashes)
|
s.router.Use(middleware.StripSlashes)
|
||||||
|
|
||||||
|
|
|
@ -253,6 +253,10 @@ func renderCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClientWebLogin(w http.ResponseWriter, r *http.Request) {
|
func handleClientWebLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !dataprovider.HasAdmin() {
|
||||||
|
http.Redirect(w, r, webAdminSetupPath, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
renderClientLoginPage(w, "")
|
renderClientLoginPage(w, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ Environment=SFTPGO_LOG_FILE_PATH=
|
||||||
EnvironmentFile=-/etc/sftpgo/sftpgo.env
|
EnvironmentFile=-/etc/sftpgo/sftpgo.env
|
||||||
ExecStart=/usr/bin/sftpgo serve
|
ExecStart=/usr/bin/sftpgo serve
|
||||||
ExecReload=/bin/kill -s HUP $MAINPID
|
ExecReload=/bin/kill -s HUP $MAINPID
|
||||||
|
LimitNOFILE=8192
|
||||||
KillMode=mixed
|
KillMode=mixed
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !noawskms
|
||||||
// +build !noawskms
|
// +build !noawskms
|
||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build noawskms
|
||||||
// +build noawskms
|
// +build noawskms
|
||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nogcpkms
|
||||||
// +build !nogcpkms
|
// +build !nogcpkms
|
||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nogcpkms
|
||||||
// +build nogcpkms
|
// +build nogcpkms
|
||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !novaultkms
|
||||||
// +build !novaultkms
|
// +build !novaultkms
|
||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build novaultkms
|
||||||
// +build novaultkms
|
// +build novaultkms
|
||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package logger
|
package logger
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !linux
|
||||||
// +build !linux
|
// +build !linux
|
||||||
|
|
||||||
package logger
|
package logger
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nometrics
|
||||||
// +build !nometrics
|
// +build !nometrics
|
||||||
|
|
||||||
// Package metrics provides Prometheus metrics support
|
// Package metrics provides Prometheus metrics support
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !noportable
|
||||||
// +build !noportable
|
// +build !noportable
|
||||||
|
|
||||||
package service
|
package service
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package service
|
package service
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package sftpd
|
package sftpd
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package sftpd
|
package sftpd
|
||||||
|
|
|
@ -241,17 +241,15 @@ func (c *Configuration) Initialize(configDir string) error {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if binding.ApplyProxyConfig {
|
if binding.ApplyProxyConfig && common.Config.ProxyProtocol > 0 {
|
||||||
proxyListener, err := common.Config.GetProxyListener(listener)
|
proxyListener, err := common.Config.GetProxyListener(listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
|
logger.Warn(logSender, "", "error enabling proxy listener: %v", err)
|
||||||
exitChannel <- err
|
exitChannel <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if proxyListener != nil {
|
|
||||||
listener = proxyListener
|
listener = proxyListener
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
exitChannel <- c.serve(listener, serverConfig)
|
exitChannel <- c.serve(listener, serverConfig)
|
||||||
}(binding)
|
}(binding)
|
||||||
|
|
1353
static/vendor/datatables/dataTables.checkboxes.css
vendored
1353
static/vendor/datatables/dataTables.checkboxes.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -99,15 +99,15 @@
|
||||||
class="user-custom">
|
class="user-custom">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-control form-control-user-custom" id="inputUsername"
|
<input type="text" class="form-control form-control-user-custom" id="inputUsername"
|
||||||
name="username" placeholder="Username" value="{{.Username}}" maxlength="60" required>
|
name="username" placeholder="Username" value="{{.Username}}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" class="form-control form-control-user-custom" id="inputPassword"
|
<input type="password" class="form-control form-control-user-custom" id="inputPassword"
|
||||||
name="password" placeholder="Password" maxlength="60" required>
|
name="password" placeholder="Password" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" class="form-control form-control-user-custom" id="inputConfirmPassword"
|
<input type="password" class="form-control form-control-user-custom" id="inputConfirmPassword"
|
||||||
name="confirm_password" placeholder="Repeat password" maxlength="60" required>
|
name="confirm_password" placeholder="Repeat password" required>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||||
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
|
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
|
||||||
|
|
|
@ -2,7 +2,7 @@ package version
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
const version = "2.1.0-dev"
|
const version = "2.1.2-dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
commit = ""
|
commit = ""
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !noazblob
|
||||||
// +build !noazblob
|
// +build !noazblob
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build noazblob
|
||||||
// +build noazblob
|
// +build noazblob
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -103,18 +103,32 @@ func (v *BaseVirtualFolder) IsLocalOrLocalCrypted() bool {
|
||||||
func (v *BaseVirtualFolder) hideConfidentialData() {
|
func (v *BaseVirtualFolder) hideConfidentialData() {
|
||||||
switch v.FsConfig.Provider {
|
switch v.FsConfig.Provider {
|
||||||
case S3FilesystemProvider:
|
case S3FilesystemProvider:
|
||||||
|
if v.FsConfig.S3Config.AccessSecret != nil {
|
||||||
v.FsConfig.S3Config.AccessSecret.Hide()
|
v.FsConfig.S3Config.AccessSecret.Hide()
|
||||||
|
}
|
||||||
case GCSFilesystemProvider:
|
case GCSFilesystemProvider:
|
||||||
|
if v.FsConfig.GCSConfig.Credentials != nil {
|
||||||
v.FsConfig.GCSConfig.Credentials.Hide()
|
v.FsConfig.GCSConfig.Credentials.Hide()
|
||||||
|
}
|
||||||
case AzureBlobFilesystemProvider:
|
case AzureBlobFilesystemProvider:
|
||||||
|
if v.FsConfig.AzBlobConfig.AccountKey != nil {
|
||||||
v.FsConfig.AzBlobConfig.AccountKey.Hide()
|
v.FsConfig.AzBlobConfig.AccountKey.Hide()
|
||||||
|
}
|
||||||
|
if v.FsConfig.AzBlobConfig.SASURL != nil {
|
||||||
v.FsConfig.AzBlobConfig.SASURL.Hide()
|
v.FsConfig.AzBlobConfig.SASURL.Hide()
|
||||||
|
}
|
||||||
case CryptedFilesystemProvider:
|
case CryptedFilesystemProvider:
|
||||||
|
if v.FsConfig.CryptConfig.Passphrase != nil {
|
||||||
v.FsConfig.CryptConfig.Passphrase.Hide()
|
v.FsConfig.CryptConfig.Passphrase.Hide()
|
||||||
|
}
|
||||||
case SFTPFilesystemProvider:
|
case SFTPFilesystemProvider:
|
||||||
|
if v.FsConfig.SFTPConfig.Password != nil {
|
||||||
v.FsConfig.SFTPConfig.Password.Hide()
|
v.FsConfig.SFTPConfig.Password.Hide()
|
||||||
|
}
|
||||||
|
if v.FsConfig.SFTPConfig.PrivateKey != nil {
|
||||||
v.FsConfig.SFTPConfig.PrivateKey.Hide()
|
v.FsConfig.SFTPConfig.PrivateKey.Hide()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForRendering prepares a folder for rendering.
|
// PrepareForRendering prepares a folder for rendering.
|
||||||
|
|
74
vfs/gcsfs.go
74
vfs/gcsfs.go
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nogcs
|
||||||
// +build !nogcs
|
// +build !nogcs
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
@ -5,7 +6,6 @@ package vfs
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
@ -112,37 +112,18 @@ func (fs *GCSFs) ConnectionID() string {
|
||||||
|
|
||||||
// Stat returns a FileInfo describing the named file
|
// Stat returns a FileInfo describing the named file
|
||||||
func (fs *GCSFs) Stat(name string) (os.FileInfo, error) {
|
func (fs *GCSFs) Stat(name string) (os.FileInfo, error) {
|
||||||
var result *FileInfo
|
|
||||||
var err error
|
|
||||||
if name == "" || name == "." {
|
if name == "" || name == "." {
|
||||||
err := fs.checkIfBucketExists()
|
err := fs.checkIfBucketExists()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewFileInfo(name, true, 0, time.Now(), false), nil
|
return NewFileInfo(name, true, 0, time.Now(), false), nil
|
||||||
}
|
}
|
||||||
if fs.config.KeyPrefix == name+"/" {
|
if fs.config.KeyPrefix == name+"/" {
|
||||||
return NewFileInfo(name, true, 0, time.Now(), false), nil
|
return NewFileInfo(name, true, 0, time.Now(), false), nil
|
||||||
}
|
}
|
||||||
attrs, err := fs.headObject(name)
|
_, info, err := fs.getObjectStat(name)
|
||||||
if err == nil {
|
return info, err
|
||||||
objSize := attrs.Size
|
|
||||||
objectModTime := attrs.Updated
|
|
||||||
isDir := attrs.ContentType == dirMimeType || strings.HasSuffix(attrs.Name, "/")
|
|
||||||
return NewFileInfo(name, isDir, objSize, objectModTime, false), nil
|
|
||||||
}
|
|
||||||
if !fs.IsNotExist(err) {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
// now check if this is a prefix (virtual directory)
|
|
||||||
hasContents, err := fs.hasContents(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if hasContents {
|
|
||||||
return NewFileInfo(name, true, 0, time.Now(), false), nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("404 no such file or directory")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lstat returns a FileInfo describing the named file
|
// Lstat returns a FileInfo describing the named file
|
||||||
|
@ -229,7 +210,7 @@ func (fs *GCSFs) Rename(source, target string) error {
|
||||||
if source == target {
|
if source == target {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fi, err := fs.Stat(source)
|
realSourceName, fi, err := fs.getObjectStat(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -241,8 +222,11 @@ func (fs *GCSFs) Rename(source, target string) error {
|
||||||
if hasContents {
|
if hasContents {
|
||||||
return fmt.Errorf("cannot rename non empty directory: %#v", source)
|
return fmt.Errorf("cannot rename non empty directory: %#v", source)
|
||||||
}
|
}
|
||||||
|
if !strings.HasSuffix(target, "/") {
|
||||||
|
target += "/"
|
||||||
}
|
}
|
||||||
src := fs.svc.Bucket(fs.config.Bucket).Object(source)
|
}
|
||||||
|
src := fs.svc.Bucket(fs.config.Bucket).Object(realSourceName)
|
||||||
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
|
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
|
||||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
|
@ -277,11 +261,18 @@ func (fs *GCSFs) Remove(name string, isDir bool) error {
|
||||||
if hasContents {
|
if hasContents {
|
||||||
return fmt.Errorf("cannot remove non empty directory: %#v", name)
|
return fmt.Errorf("cannot remove non empty directory: %#v", name)
|
||||||
}
|
}
|
||||||
|
if !strings.HasSuffix(name, "/") {
|
||||||
|
name += "/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
|
|
||||||
err := fs.svc.Bucket(fs.config.Bucket).Object(name).Delete(ctx)
|
err := fs.svc.Bucket(fs.config.Bucket).Object(name).Delete(ctx)
|
||||||
|
if fs.IsNotExist(err) && isDir {
|
||||||
|
// we can have directories without a trailing "/" (created using v2.1.0 and before)
|
||||||
|
err = fs.svc.Bucket(fs.config.Bucket).Object(strings.TrimSuffix(name, "/")).Delete(ctx)
|
||||||
|
}
|
||||||
metrics.GCSDeleteObjectCompleted(err)
|
metrics.GCSDeleteObjectCompleted(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -292,6 +283,9 @@ func (fs *GCSFs) Mkdir(name string) error {
|
||||||
if !fs.IsNotExist(err) {
|
if !fs.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !strings.HasSuffix(name, "/") {
|
||||||
|
name += "/"
|
||||||
|
}
|
||||||
_, w, _, err := fs.Create(name, -1)
|
_, w, _, err := fs.Create(name, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -613,6 +607,36 @@ func (fs *GCSFs) resolve(name string, prefix string) (string, bool) {
|
||||||
return result, isDir
|
return result, isDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getObjectStat returns the stat result and the real object name as first value
|
||||||
|
func (fs *GCSFs) getObjectStat(name string) (string, os.FileInfo, error) {
|
||||||
|
attrs, err := fs.headObject(name)
|
||||||
|
if err == nil {
|
||||||
|
objSize := attrs.Size
|
||||||
|
objectModTime := attrs.Updated
|
||||||
|
isDir := attrs.ContentType == dirMimeType || strings.HasSuffix(attrs.Name, "/")
|
||||||
|
return name, NewFileInfo(name, isDir, objSize, objectModTime, false), nil
|
||||||
|
}
|
||||||
|
if !fs.IsNotExist(err) {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
// now check if this is a prefix (virtual directory)
|
||||||
|
hasContents, err := fs.hasContents(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if hasContents {
|
||||||
|
return name, NewFileInfo(name, true, 0, time.Now(), false), nil
|
||||||
|
}
|
||||||
|
// finally check if this is an object with a trailing /
|
||||||
|
attrs, err = fs.headObject(name + "/")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
objSize := attrs.Size
|
||||||
|
objectModTime := attrs.Updated
|
||||||
|
return name + "/", NewFileInfo(name, true, objSize, objectModTime, false), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (fs *GCSFs) checkIfBucketExists() error {
|
func (fs *GCSFs) checkIfBucketExists() error {
|
||||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nogcs
|
||||||
// +build nogcs
|
// +build nogcs
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
34
vfs/s3fs.go
34
vfs/s3fs.go
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !nos3
|
||||||
// +build !nos3
|
// +build !nos3
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
@ -16,6 +17,7 @@ import (
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/request"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
|
@ -178,6 +180,17 @@ func (fs *S3Fs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, fun
|
||||||
}
|
}
|
||||||
ctx, cancelFn := context.WithCancel(context.Background())
|
ctx, cancelFn := context.WithCancel(context.Background())
|
||||||
downloader := s3manager.NewDownloaderWithClient(fs.svc)
|
downloader := s3manager.NewDownloaderWithClient(fs.svc)
|
||||||
|
if offset == 0 {
|
||||||
|
downloader.RequestOptions = append(downloader.RequestOptions, func(r *request.Request) {
|
||||||
|
chunkCtx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
|
||||||
|
r.SetContext(chunkCtx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
var streamRange *string
|
var streamRange *string
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
streamRange = aws.String(fmt.Sprintf("bytes=%v-", offset))
|
streamRange = aws.String(fmt.Sprintf("bytes=%v-", offset))
|
||||||
|
@ -278,11 +291,19 @@ func (fs *S3Fs) Rename(source, target string) error {
|
||||||
defer cancelFn()
|
defer cancelFn()
|
||||||
_, err = fs.svc.CopyObjectWithContext(ctx, &s3.CopyObjectInput{
|
_, err = fs.svc.CopyObjectWithContext(ctx, &s3.CopyObjectInput{
|
||||||
Bucket: aws.String(fs.config.Bucket),
|
Bucket: aws.String(fs.config.Bucket),
|
||||||
CopySource: aws.String(url.PathEscape(copySource)),
|
CopySource: aws.String(pathEscape(copySource)),
|
||||||
Key: aws.String(target),
|
Key: aws.String(target),
|
||||||
StorageClass: utils.NilIfEmpty(fs.config.StorageClass),
|
StorageClass: utils.NilIfEmpty(fs.config.StorageClass),
|
||||||
ContentType: utils.NilIfEmpty(contentType),
|
ContentType: utils.NilIfEmpty(contentType),
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
metrics.S3CopyObjectCompleted(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = fs.svc.WaitUntilObjectExistsWithContext(ctx, &s3.HeadObjectInput{
|
||||||
|
Bucket: aws.String(fs.config.Bucket),
|
||||||
|
Key: aws.String(target),
|
||||||
|
})
|
||||||
metrics.S3CopyObjectCompleted(err)
|
metrics.S3CopyObjectCompleted(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -686,3 +707,14 @@ func (*S3Fs) Close() error {
|
||||||
func (*S3Fs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
|
func (*S3Fs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
|
||||||
return nil, ErrStorageSizeUnavailable
|
return nil, ErrStorageSizeUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ideally we should simply use url.PathEscape:
|
||||||
|
//
|
||||||
|
// https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/go/example_code/s3/s3_copy_object.go#L65
|
||||||
|
//
|
||||||
|
// but this cause issue with some vendors, see #483, the code below is copied from rclone
|
||||||
|
func pathEscape(in string) string {
|
||||||
|
var u url.URL
|
||||||
|
u.Path = in
|
||||||
|
return strings.ReplaceAll(u.String(), "+", "%2B")
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build nos3
|
||||||
// +build nos3
|
// +build nos3
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -455,6 +455,9 @@ func (*SFTPFs) IsNotExist(err error) bool {
|
||||||
// IsPermission returns a boolean indicating whether the error is known to
|
// IsPermission returns a boolean indicating whether the error is known to
|
||||||
// report that permission is denied.
|
// report that permission is denied.
|
||||||
func (*SFTPFs) IsPermission(err error) bool {
|
func (*SFTPFs) IsPermission(err error) bool {
|
||||||
|
if _, ok := err.(*pathResolutionError); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return os.IsPermission(err)
|
return os.IsPermission(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -612,11 +615,11 @@ func (fs *SFTPFs) isSubDir(name string) error {
|
||||||
}
|
}
|
||||||
if len(name) < len(fs.config.Prefix) {
|
if len(name) < len(fs.config.Prefix) {
|
||||||
err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
|
err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
|
||||||
return err
|
return &pathResolutionError{err: err.Error()}
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(name, fs.config.Prefix+"/") {
|
if !strings.HasPrefix(name, fs.config.Prefix+"/") {
|
||||||
err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
|
err := fmt.Errorf("path %#v is not inside: %#v", name, fs.config.Prefix)
|
||||||
return err
|
return &pathResolutionError{err: err.Error()}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !darwin && !linux && !freebsd
|
||||||
// +build !darwin,!linux,!freebsd
|
// +build !darwin,!linux,!freebsd
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build linux
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build freebsd || darwin
|
||||||
// +build freebsd darwin
|
// +build freebsd darwin
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package vfs
|
package vfs
|
||||||
|
|
|
@ -367,7 +367,7 @@ func writeLog(r *http.Request, err error) {
|
||||||
|
|
||||||
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
|
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
|
||||||
metrics.AddLoginAttempt(loginMethod)
|
metrics.AddLoginAttempt(loginMethod)
|
||||||
if err != nil && err != common.ErrInternalFailure {
|
if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
|
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
|
||||||
event := common.HostEventLoginFailed
|
event := common.HostEventLoginFailed
|
||||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -651,6 +652,110 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
||||||
assert.Len(t, common.Connections.GetStats(), 0)
|
assert.Len(t, common.Connections.GetStats(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLockAfterDelete(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
client := getWebDavClient(user, false, nil)
|
||||||
|
assert.NoError(t, checkBasicFunc(client))
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = uploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
lockBody := `<?xml version="1.0" encoding="utf-8" ?><d:lockinfo xmlns:d="DAV:"><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype></d:lockinfo>`
|
||||||
|
req, err := http.NewRequest("LOCK", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.SetBasicAuth(u.Username, u.Password)
|
||||||
|
httpClient := httpclient.GetHTTPClient()
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
response, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
re := regexp.MustCompile(`\<D:locktoken><D:href>.*</D:href>`)
|
||||||
|
lockToken := string(re.Find(response))
|
||||||
|
lockToken = strings.Replace(lockToken, "<D:locktoken><D:href>", "", 1)
|
||||||
|
lockToken = strings.Replace(lockToken, "</D:href>", "", 1)
|
||||||
|
err = resp.Body.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("If", fmt.Sprintf("(%v)", lockToken))
|
||||||
|
req.SetBasicAuth(u.Username, u.Password)
|
||||||
|
resp, err = httpClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||||
|
err = resp.Body.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// if we try to lock again it must succeed, the lock must be deleted with the object
|
||||||
|
req, err = http.NewRequest("LOCK", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.SetBasicAuth(u.Username, u.Password)
|
||||||
|
resp, err = httpClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
err = resp.Body.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenameWithLock(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
client := getWebDavClient(user, false, nil)
|
||||||
|
assert.NoError(t, checkBasicFunc(client))
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(65535)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = uploadFile(testFilePath, testFileName, testFileSize, client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
lockBody := `<?xml version="1.0" encoding="utf-8" ?><d:lockinfo xmlns:d="DAV:"><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype></d:lockinfo>`
|
||||||
|
req, err := http.NewRequest("LOCK", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), bytes.NewReader([]byte(lockBody)))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.SetBasicAuth(u.Username, u.Password)
|
||||||
|
httpClient := httpclient.GetHTTPClient()
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
response, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
re := regexp.MustCompile(`\<D:locktoken><D:href>.*</D:href>`)
|
||||||
|
lockToken := string(re.Find(response))
|
||||||
|
lockToken = strings.Replace(lockToken, "<D:locktoken><D:href>", "", 1)
|
||||||
|
lockToken = strings.Replace(lockToken, "</D:href>", "", 1)
|
||||||
|
err = resp.Body.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// MOVE with a lock should succeeded
|
||||||
|
req, err = http.NewRequest("MOVE", fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("If", fmt.Sprintf("(%v)", lockToken))
|
||||||
|
req.Header.Set("Overwrite", "T")
|
||||||
|
req.Header.Set("Destination", path.Join("/", testFileName+"1"))
|
||||||
|
req.SetBasicAuth(u.Username, u.Password)
|
||||||
|
resp, err = httpClient.Do(req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
err = resp.Body.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPropPatch(t *testing.T) {
|
func TestPropPatch(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Username = u.Username + "1"
|
u.Username = u.Username + "1"
|
||||||
|
|
Loading…
Reference in a new issue