improve FTP support
- allow to disable active mode - allow to disable SITE commands - add optional support for calculating hash value of files - add optional support for the non standard COMB command
This commit is contained in:
parent
5b1d8666b3
commit
1dce1eff48
15 changed files with 375 additions and 31 deletions
|
@ -612,16 +612,23 @@ func (c ConnectionStatus) GetConnectionDuration() string {
|
||||||
|
|
||||||
// GetConnectionInfo returns connection info.
|
// GetConnectionInfo returns connection info.
|
||||||
// Protocol,Client Version and RemoteAddress are returned.
|
// Protocol,Client Version and RemoteAddress are returned.
|
||||||
// For SSH commands the issued command is returned too.
|
|
||||||
func (c ConnectionStatus) GetConnectionInfo() string {
|
func (c ConnectionStatus) GetConnectionInfo() string {
|
||||||
result := fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress)
|
var result strings.Builder
|
||||||
if c.Protocol == ProtocolSSH && len(c.Command) > 0 {
|
|
||||||
result += fmt.Sprintf(". Command: %#v", c.Command)
|
result.WriteString(fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress))
|
||||||
|
|
||||||
|
if c.Command == "" {
|
||||||
|
return result.String()
|
||||||
}
|
}
|
||||||
if c.Protocol == ProtocolWebDAV && len(c.Command) > 0 {
|
|
||||||
result += fmt.Sprintf(". Method: %#v", c.Command)
|
switch c.Protocol {
|
||||||
|
case ProtocolSSH, ProtocolFTP:
|
||||||
|
result.WriteString(fmt.Sprintf(". Command: %#v", c.Command))
|
||||||
|
case ProtocolWebDAV:
|
||||||
|
result.WriteString(fmt.Sprintf(". Method: %#v", c.Command))
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTransfersAsString returns the active transfers as string
|
// GetTransfersAsString returns the active transfers as string
|
||||||
|
|
|
@ -113,6 +113,10 @@ func Init() {
|
||||||
Start: 50000,
|
Start: 50000,
|
||||||
End: 50100,
|
End: 50100,
|
||||||
},
|
},
|
||||||
|
DisableActiveMode: false,
|
||||||
|
EnableSite: false,
|
||||||
|
HASHSupport: 0,
|
||||||
|
CombineSupport: 0,
|
||||||
CertificateFile: "",
|
CertificateFile: "",
|
||||||
CertificateKeyFile: "",
|
CertificateKeyFile: "",
|
||||||
},
|
},
|
||||||
|
@ -660,6 +664,10 @@ func setViperDefaults() {
|
||||||
viper.SetDefault("ftpd.active_transfers_port_non_20", globalConf.FTPD.ActiveTransfersPortNon20)
|
viper.SetDefault("ftpd.active_transfers_port_non_20", globalConf.FTPD.ActiveTransfersPortNon20)
|
||||||
viper.SetDefault("ftpd.passive_port_range.start", globalConf.FTPD.PassivePortRange.Start)
|
viper.SetDefault("ftpd.passive_port_range.start", globalConf.FTPD.PassivePortRange.Start)
|
||||||
viper.SetDefault("ftpd.passive_port_range.end", globalConf.FTPD.PassivePortRange.End)
|
viper.SetDefault("ftpd.passive_port_range.end", globalConf.FTPD.PassivePortRange.End)
|
||||||
|
viper.SetDefault("ftpd.disable_active_mode", globalConf.FTPD.DisableActiveMode)
|
||||||
|
viper.SetDefault("ftpd.enable_site", globalConf.FTPD.EnableSite)
|
||||||
|
viper.SetDefault("ftpd.hash_support", globalConf.FTPD.HASHSupport)
|
||||||
|
viper.SetDefault("ftpd.combine_support", globalConf.FTPD.CombineSupport)
|
||||||
viper.SetDefault("ftpd.certificate_file", globalConf.FTPD.CertificateFile)
|
viper.SetDefault("ftpd.certificate_file", globalConf.FTPD.CertificateFile)
|
||||||
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
|
viper.SetDefault("ftpd.certificate_key_file", globalConf.FTPD.CertificateKeyFile)
|
||||||
viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile)
|
viper.SetDefault("webdavd.certificate_file", globalConf.WebDAVD.CertificateFile)
|
||||||
|
|
|
@ -104,6 +104,10 @@ The configuration file contains the following sections:
|
||||||
- `active_transfers_port_non_20`, boolean. Do not impose the port 20 for active data transfers. Enabling this option allows to run SFTPGo with less privilege. Default: false.
|
- `active_transfers_port_non_20`, boolean. Do not impose the port 20 for active data transfers. Enabling this option allows to run SFTPGo with less privilege. Default: false.
|
||||||
- `force_passive_ip`, ip address. Deprecated, please use `bindings`
|
- `force_passive_ip`, ip address. Deprecated, please use `bindings`
|
||||||
- `passive_port_range`, struct containing the key `start` and `end`. Port Range for data connections. Random if not specified. Default range is 50000-50100.
|
- `passive_port_range`, struct containing the key `start` and `end`. Port Range for data connections. Random if not specified. Default range is 50000-50100.
|
||||||
|
- `disable_active_mode`, boolean. Set to `true` to disable active FTP, default `false`.
|
||||||
|
- `enable_site`, boolean. Set to true to enable the FTP SITE command. We support `chmod` and `symlink` if SITE support is enabled. Default `false`
|
||||||
|
- `hash_support`, integer. Set to `1` to enable FTP commands that allow to calculate the hash value of files. These FTP commands will be enabled: `HASH`, `XCRC`, `MD5/XMD5`, `XSHA/XSHA1`, `XSHA256`, `XSHA512`. Please keep in mind that to calculate the hash we need to read the whole file, for remote backends this means downloading the file, for the encrypted backend this means decrypting the file. Default `0`.
|
||||||
|
- `combine_support`, integer. Set to 1 to enable support for the non standard `COMB` FTP command. Combine is only supported for local filesystem, for cloud backends it has no advantage as it will download the partial files and will upload the combined one. Cloud backends natively support multipart uploads. Default `0`.
|
||||||
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
|
- `certificate_file`, string. Certificate for FTPS. This can be an absolute path or a path relative to the config dir.
|
||||||
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
- `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. A certificate and the private key are required to enable explicit and implicit TLS. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
|
||||||
- `tls_mode`, integer. Deprecated, please use `bindings`
|
- `tls_mode`, integer. Deprecated, please use `bindings`
|
||||||
|
|
25
ftpd/ftpd.go
25
ftpd/ftpd.go
|
@ -27,9 +27,10 @@ type Binding struct {
|
||||||
Address string `json:"address" mapstructure:"address"`
|
Address string `json:"address" mapstructure:"address"`
|
||||||
// The port used for serving requests
|
// The port used for serving requests
|
||||||
Port int `json:"port" mapstructure:"port"`
|
Port int `json:"port" mapstructure:"port"`
|
||||||
// apply the proxy configuration, if any, for this binding
|
// Apply the proxy configuration, if any, for this binding
|
||||||
ApplyProxyConfig bool `json:"apply_proxy_config" mapstructure:"apply_proxy_config"`
|
ApplyProxyConfig bool `json:"apply_proxy_config" mapstructure:"apply_proxy_config"`
|
||||||
// set to 1 to require TLS for both data and control connection
|
// Set to 1 to require TLS for both data and control connection.
|
||||||
|
// Set to 2 to enable implicit TLS
|
||||||
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
|
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
|
||||||
// External IP address to expose for passive connections.
|
// External IP address to expose for passive connections.
|
||||||
ForcePassiveIP string `json:"force_passive_ip" mapstructure:"force_passive_ip"`
|
ForcePassiveIP string `json:"force_passive_ip" mapstructure:"force_passive_ip"`
|
||||||
|
@ -103,10 +104,26 @@ type Configuration struct {
|
||||||
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
|
CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
|
||||||
// Do not impose the port 20 for active data transfer. Enabling this option allows to run SFTPGo with less privilege
|
// Do not impose the port 20 for active data transfer. Enabling this option allows to run SFTPGo with less privilege
|
||||||
ActiveTransfersPortNon20 bool `json:"active_transfers_port_non_20" mapstructure:"active_transfers_port_non_20"`
|
ActiveTransfersPortNon20 bool `json:"active_transfers_port_non_20" mapstructure:"active_transfers_port_non_20"`
|
||||||
// Port Range for data connections. Random if not specified
|
// Set to true to disable active FTP
|
||||||
PassivePortRange PortRange `json:"passive_port_range" mapstructure:"passive_port_range"`
|
DisableActiveMode bool `json:"disable_active_mode" mapstructure:"disable_active_mode"`
|
||||||
|
// Set to true to enable the FTP SITE command.
|
||||||
|
// We support chmod and symlink if SITE support is enabled
|
||||||
|
EnableSite bool `json:"enable_site" mapstructure:"enable_site"`
|
||||||
|
// Set to 1 to enable FTP commands that allow to calculate the hash value of files.
|
||||||
|
// These FTP commands will be enabled: HASH, XCRC, MD5/XMD5, XSHA/XSHA1, XSHA256, XSHA512.
|
||||||
|
// Please keep in mind that to calculate the hash we need to read the whole file, for
|
||||||
|
// remote backends this means downloading the file, for the encrypted backend this means
|
||||||
|
// decrypting the file
|
||||||
|
HASHSupport int `json:"hash_support" mapstructure:"hash_support"`
|
||||||
|
// Set to 1 to enable support for the non standard "COMB" FTP command.
|
||||||
|
// Combine is only supported for local filesystem, for cloud backends it has
|
||||||
|
// no advantage as it will download the partial files and will upload the
|
||||||
|
// combined one. Cloud backends natively support multipart uploads.
|
||||||
|
CombineSupport int `json:"combine_support" mapstructure:"combine_support"`
|
||||||
// Deprecated: please use Bindings
|
// Deprecated: please use Bindings
|
||||||
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
|
TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
|
||||||
|
// Port Range for data connections. Random if not specified
|
||||||
|
PassivePortRange PortRange `json:"passive_port_range" mapstructure:"passive_port_range"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBind returns true if there is at least a valid binding
|
// ShouldBind returns true if there is at least a valid binding
|
||||||
|
|
|
@ -2,7 +2,9 @@ package ftpd_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -37,6 +39,7 @@ const (
|
||||||
logSender = "ftpdTesting"
|
logSender = "ftpdTesting"
|
||||||
ftpServerAddr = "127.0.0.1:2121"
|
ftpServerAddr = "127.0.0.1:2121"
|
||||||
sftpServerAddr = "127.0.0.1:2122"
|
sftpServerAddr = "127.0.0.1:2122"
|
||||||
|
ftpSrvAddrTLS = "127.0.0.1:2124" // ftp server with implicit tls
|
||||||
defaultUsername = "test_user_ftp"
|
defaultUsername = "test_user_ftp"
|
||||||
defaultPassword = "test_password"
|
defaultPassword = "test_password"
|
||||||
configDir = ".."
|
configDir = ".."
|
||||||
|
@ -157,6 +160,7 @@ func TestMain(m *testing.M) {
|
||||||
ftpdConf.BannerFile = bannerFileName
|
ftpdConf.BannerFile = bannerFileName
|
||||||
ftpdConf.CertificateFile = certPath
|
ftpdConf.CertificateFile = certPath
|
||||||
ftpdConf.CertificateKeyFile = keyPath
|
ftpdConf.CertificateKeyFile = keyPath
|
||||||
|
ftpdConf.EnableSite = true
|
||||||
|
|
||||||
// required to test sftpfs
|
// required to test sftpfs
|
||||||
sftpdConf := config.GetSFTPDConfig()
|
sftpdConf := config.GetSFTPDConfig()
|
||||||
|
@ -165,7 +169,8 @@ func TestMain(m *testing.M) {
|
||||||
Port: 2122,
|
Port: 2122,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
sftpdConf.HostKeys = []string{filepath.Join(os.TempDir(), "id_ed25519")}
|
hostKeyPath := filepath.Join(os.TempDir(), "id_ed25519")
|
||||||
|
sftpdConf.HostKeys = []string{hostKeyPath}
|
||||||
|
|
||||||
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
||||||
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
||||||
|
@ -205,6 +210,35 @@ func TestMain(m *testing.M) {
|
||||||
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
|
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
|
||||||
ftpd.ReloadTLSCertificate() //nolint:errcheck
|
ftpd.ReloadTLSCertificate() //nolint:errcheck
|
||||||
|
|
||||||
|
ftpdConf = config.GetFTPDConfig()
|
||||||
|
ftpdConf.Bindings = []ftpd.Binding{
|
||||||
|
{
|
||||||
|
Port: 2124,
|
||||||
|
TLSMode: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ftpdConf.CertificateFile = certPath
|
||||||
|
ftpdConf.CertificateKeyFile = keyPath
|
||||||
|
ftpdConf.EnableSite = false
|
||||||
|
ftpdConf.DisableActiveMode = true
|
||||||
|
ftpdConf.CombineSupport = 1
|
||||||
|
ftpdConf.HASHSupport = 1
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logger.Debug(logSender, "", "initializing FTP server with config %+v", ftpdConf)
|
||||||
|
if err := ftpdConf.Initialize(configDir); err != nil {
|
||||||
|
logger.ErrorToConsole("could not start FTP server: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
waitTCPListening(ftpdConf.Bindings[0].GetAddress())
|
||||||
|
|
||||||
|
// ensure all the initial connections to check if the service is alive are disconnected
|
||||||
|
for len(common.Connections.GetStats()) > 0 {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
exitCode := m.Run()
|
exitCode := m.Run()
|
||||||
os.Remove(logFilePath)
|
os.Remove(logFilePath)
|
||||||
os.Remove(bannerFile)
|
os.Remove(bannerFile)
|
||||||
|
@ -213,6 +247,8 @@ func TestMain(m *testing.M) {
|
||||||
os.Remove(postConnectPath)
|
os.Remove(postConnectPath)
|
||||||
os.Remove(certPath)
|
os.Remove(certPath)
|
||||||
os.Remove(keyPath)
|
os.Remove(keyPath)
|
||||||
|
os.Remove(hostKeyPath)
|
||||||
|
os.Remove(hostKeyPath + ".pub")
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1578,6 +1614,218 @@ func TestChmod(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCombineDisabled(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||||
|
client, err := getFTPClient(user, true)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
code, response, err := client.SendCustomCommand("COMB file file.1 file.2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusNotImplemented, code)
|
||||||
|
assert.Equal(t, "COMB support is disabled", response)
|
||||||
|
|
||||||
|
err = client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveModeDisabled(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
client, err := getFTPClientImplicitTLS(user)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
code, response, err := client.SendCustomCommand("PORT 10,2,0,2,4,31")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusNotAvailable, code)
|
||||||
|
assert.Equal(t, "PORT command is disabled", response)
|
||||||
|
|
||||||
|
code, response, err = client.SendCustomCommand("EPRT |1|132.235.1.2|6275|")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusNotAvailable, code)
|
||||||
|
assert.Equal(t, "EPRT command is disabled", response)
|
||||||
|
|
||||||
|
err = client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err = getFTPClient(user, false)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
code, response, err := client.SendCustomCommand("PORT 10,2,0,2,4,31")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusCommandOK, code)
|
||||||
|
assert.Equal(t, "PORT command successful", response)
|
||||||
|
|
||||||
|
code, response, err = client.SendCustomCommand("EPRT |1|132.235.1.2|6275|")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusCommandOK, code)
|
||||||
|
assert.Equal(t, "EPRT command successful", response)
|
||||||
|
|
||||||
|
err = client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSITEDisabled(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
client, err := getFTPClientImplicitTLS(user)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
code, response, err := client.SendCustomCommand("SITE CHMOD 600 afile.txt")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusBadCommand, code)
|
||||||
|
assert.Equal(t, "SITE support is disabled", response)
|
||||||
|
|
||||||
|
err = client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHASH(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
u = getTestUserWithCryptFs()
|
||||||
|
u.Username += "_crypt"
|
||||||
|
cryptUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, user := range []dataprovider.User{localUser, sftpUser, cryptUser} {
|
||||||
|
client, err := getFTPClientImplicitTLS(user)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(131072)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
f, err := os.Open(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = io.Copy(h, f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
hash := hex.EncodeToString(h.Sum(nil))
|
||||||
|
err = f.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
code, response, err := client.SendCustomCommand(fmt.Sprintf("XSHA256 %v", testFileName))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusRequestedFileActionOK, code)
|
||||||
|
assert.Contains(t, response, hash)
|
||||||
|
|
||||||
|
code, response, err = client.SendCustomCommand(fmt.Sprintf("HASH %v", testFileName))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ftp.StatusFile, code)
|
||||||
|
assert.Contains(t, response, hash)
|
||||||
|
|
||||||
|
err = client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.Remove(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if user.Username == defaultUsername {
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = httpd.RemoveUser(cryptUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(cryptUser.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCombine(t *testing.T) {
|
||||||
|
u := getTestUser()
|
||||||
|
localUser, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
for _, user := range []dataprovider.User{localUser, sftpUser} {
|
||||||
|
client, err := getFTPClientImplicitTLS(user)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
testFileSize := int64(131072)
|
||||||
|
err = createTestFile(testFilePath, testFileSize)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = checkBasicFTP(client)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName+".1", testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = ftpUploadFile(testFilePath, testFileName+".2", testFileSize, client, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
code, response, err := client.SendCustomCommand(fmt.Sprintf("COMB %v %v %v", testFileName, testFileName+".1", testFileName+".2"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if user.Username == defaultUsername {
|
||||||
|
assert.Equal(t, ftp.StatusRequestedFileActionOK, code)
|
||||||
|
assert.Equal(t, "COMB succeeded!", response)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, ftp.StatusFileUnavailable, code)
|
||||||
|
assert.Contains(t, response, "COMB is not supported for this filesystem")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Quit()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.Remove(testFilePath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if user.Username == defaultUsername {
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = httpd.RemoveUser(localUser, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func checkBasicFTP(client *ftp.ServerConn) error {
|
func checkBasicFTP(client *ftp.ServerConn) error {
|
||||||
_, err := client.CurrentDir()
|
_, err := client.CurrentDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1647,6 +1895,30 @@ func ftpDownloadFile(remoteSourcePath string, localDestPath string, expectedSize
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFTPClientImplicitTLS(user dataprovider.User) (*ftp.ServerConn, error) {
|
||||||
|
ftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: "localhost",
|
||||||
|
InsecureSkipVerify: true, // use this for tests only
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
ftpOptions = append(ftpOptions, ftp.DialWithTLS(tlsConfig))
|
||||||
|
ftpOptions = append(ftpOptions, ftp.DialWithDisabledEPSV(true))
|
||||||
|
client, err := ftp.Dial(ftpSrvAddrTLS, ftpOptions...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pwd := defaultPassword
|
||||||
|
if user.Password != "" {
|
||||||
|
pwd = user.Password
|
||||||
|
}
|
||||||
|
err = client.Login(user.Username, pwd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
func getFTPClient(user dataprovider.User, useTLS bool) (*ftp.ServerConn, error) {
|
func getFTPClient(user dataprovider.User, useTLS bool) (*ftp.ServerConn, error) {
|
||||||
ftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}
|
ftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}
|
||||||
if useTLS {
|
if useTLS {
|
||||||
|
@ -1662,7 +1934,7 @@ func getFTPClient(user dataprovider.User, useTLS bool) (*ftp.ServerConn, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pwd := defaultPassword
|
pwd := defaultPassword
|
||||||
if len(user.Password) > 0 {
|
if user.Password != "" {
|
||||||
pwd = user.Password
|
pwd = user.Password
|
||||||
}
|
}
|
||||||
err = client.Login(user.Username, pwd)
|
err = client.Login(user.Username, pwd)
|
||||||
|
|
|
@ -17,7 +17,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNotImplemented = errors.New("Not implemented")
|
errNotImplemented = errors.New("Not implemented")
|
||||||
|
errCOMBNotSupported = errors.New("COMB is not supported for this filesystem")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connection details for an FTP connection.
|
// Connection details for an FTP connection.
|
||||||
|
@ -45,12 +46,12 @@ func (c *Connection) GetRemoteAddress() string {
|
||||||
|
|
||||||
// Disconnect disconnects the client
|
// Disconnect disconnects the client
|
||||||
func (c *Connection) Disconnect() error {
|
func (c *Connection) Disconnect() error {
|
||||||
return c.clientContext.Close(0, "")
|
return c.clientContext.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommand returns an empty string
|
// GetCommand returns an empty string
|
||||||
func (c *Connection) GetCommand() string {
|
func (c *Connection) GetCommand() string {
|
||||||
return ""
|
return c.clientContext.GetLastCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create is not implemented we use ClientDriverExtentionFileTransfer
|
// Create is not implemented we use ClientDriverExtentionFileTransfer
|
||||||
|
@ -285,6 +286,11 @@ func (c *Connection) GetHandle(name string, flags int, offset int64) (ftpserver.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, c.GetFsError(err)
|
return nil, c.GetFsError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.GetCommand() == "COMB" && !vfs.IsLocalOsFs(c.Fs) {
|
||||||
|
return nil, errCOMBNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
if flags&os.O_WRONLY != 0 {
|
if flags&os.O_WRONLY != 0 {
|
||||||
return c.uploadFile(p, name, flags)
|
return c.uploadFile(p, name, flags)
|
||||||
}
|
}
|
||||||
|
@ -384,10 +390,11 @@ func (c *Connection) handleFTPUploadToExistingFile(flags int, resolvedPath, file
|
||||||
return nil, common.ErrQuotaExceeded
|
return nil, common.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
minWriteOffset := int64(0)
|
minWriteOffset := int64(0)
|
||||||
// ftpserverlib set os.O_WRONLY | os.O_APPEND for APPE
|
// ftpserverlib sets:
|
||||||
// and os.O_WRONLY | os.O_CREATE for REST. If is not APPE
|
// - os.O_WRONLY | os.O_APPEND for APPE and COMB
|
||||||
// and REST = 0 then os.O_WRONLY | os.O_CREATE | os.O_TRUNC
|
// - os.O_WRONLY | os.O_CREATE for REST.
|
||||||
// so if we don't have O_TRUC is a resume
|
// - os.O_WRONLY | os.O_CREATE | os.O_TRUNC if the command is not APPE and REST = 0
|
||||||
|
// so if we don't have O_TRUNC is a resume.
|
||||||
isResume := flags&os.O_TRUNC == 0
|
isResume := flags&os.O_TRUNC == 0
|
||||||
// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace
|
// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace
|
||||||
// will return false in this case and we deny the upload before
|
// will return false in this case and we deny the upload before
|
||||||
|
|
|
@ -51,10 +51,22 @@ func (cc mockFTPClientContext) GetClientVersion() string {
|
||||||
return "mock version"
|
return "mock version"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cc mockFTPClientContext) Close(code int, message string) error {
|
func (cc mockFTPClientContext) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cc mockFTPClientContext) HasTLSForControl() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc mockFTPClientContext) HasTLSForTransfers() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc mockFTPClientContext) GetLastCommand() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// MockOsFs mockable OsFs
|
// MockOsFs mockable OsFs
|
||||||
type MockOsFs struct {
|
type MockOsFs struct {
|
||||||
vfs.Fs
|
vfs.Fs
|
||||||
|
@ -209,7 +221,7 @@ func TestUserInvalidParams(t *testing.T) {
|
||||||
End: 11000,
|
End: 11000,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
server := NewServer(c, configDir, binding, 0)
|
server := NewServer(c, configDir, binding, 3)
|
||||||
_, err := server.validateUser(u, mockFTPClientContext{})
|
_, err := server.validateUser(u, mockFTPClientContext{})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
@ -241,7 +253,7 @@ func TestUserInvalidParams(t *testing.T) {
|
||||||
|
|
||||||
func TestClientVersion(t *testing.T) {
|
func TestClientVersion(t *testing.T) {
|
||||||
mockCC := mockFTPClientContext{}
|
mockCC := mockFTPClientContext{}
|
||||||
connID := fmt.Sprintf("%v", mockCC.ID())
|
connID := fmt.Sprintf("2_%v", mockCC.ID())
|
||||||
user := dataprovider.User{}
|
user := dataprovider.User{}
|
||||||
connection := &Connection{
|
connection := &Connection{
|
||||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, user, nil),
|
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, user, nil),
|
||||||
|
@ -258,7 +270,7 @@ func TestClientVersion(t *testing.T) {
|
||||||
|
|
||||||
func TestDriverMethodsNotImplemented(t *testing.T) {
|
func TestDriverMethodsNotImplemented(t *testing.T) {
|
||||||
mockCC := mockFTPClientContext{}
|
mockCC := mockFTPClientContext{}
|
||||||
connID := fmt.Sprintf("%v", mockCC.ID())
|
connID := fmt.Sprintf("2_%v", mockCC.ID())
|
||||||
user := dataprovider.User{}
|
user := dataprovider.User{}
|
||||||
connection := &Connection{
|
connection := &Connection{
|
||||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, user, nil),
|
BaseConnection: common.NewBaseConnection(connID, common.ProtocolFTP, user, nil),
|
||||||
|
|
|
@ -93,6 +93,10 @@ func (s *Server) GetSettings() (*ftpserver.Settings, error) {
|
||||||
ConnectionTimeout: 20,
|
ConnectionTimeout: 20,
|
||||||
Banner: s.statusBanner,
|
Banner: s.statusBanner,
|
||||||
TLSRequired: ftpserver.TLSRequirement(s.binding.TLSMode),
|
TLSRequired: ftpserver.TLSRequirement(s.binding.TLSMode),
|
||||||
|
DisableSite: !s.config.EnableSite,
|
||||||
|
DisableActiveMode: s.config.DisableActiveMode,
|
||||||
|
EnableHASH: s.config.HASHSupport > 0,
|
||||||
|
EnableCOMB: s.config.CombineSupport > 0,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +160,7 @@ func (s *Server) GetTLSConfig() (*tls.Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext) (*Connection, error) {
|
func (s *Server) validateUser(user dataprovider.User, cc ftpserver.ClientContext) (*Connection, error) {
|
||||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolFTP, cc.ID())
|
connectionID := fmt.Sprintf("%v_%v_%v", common.ProtocolFTP, s.ID, cc.ID())
|
||||||
if !filepath.IsAbs(user.HomeDir) {
|
if !filepath.IsAbs(user.HomeDir) {
|
||||||
logger.Warn(logSender, connectionID, "user %#v has an invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
|
logger.Warn(logSender, connectionID, "user %#v has an invalid home dir: %#v. Home dir must be an absolute path, login not allowed",
|
||||||
user.Username, user.HomeDir)
|
user.Username, user.HomeDir)
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -11,7 +11,7 @@ require (
|
||||||
github.com/aws/aws-sdk-go v1.36.13
|
github.com/aws/aws-sdk-go v1.36.13
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
|
||||||
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
|
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
|
||||||
github.com/fclairamb/ftpserverlib v0.11.0
|
github.com/fclairamb/ftpserverlib v0.11.1-0.20201224162151-6345897942b7
|
||||||
github.com/frankban/quicktest v1.11.2 // indirect
|
github.com/frankban/quicktest v1.11.2 // indirect
|
||||||
github.com/go-chi/chi v1.5.1
|
github.com/go-chi/chi v1.5.1
|
||||||
github.com/go-chi/render v1.0.1
|
github.com/go-chi/render v1.0.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -178,8 +178,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
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.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/fclairamb/ftpserverlib v0.11.0 h1:leyKdtf3Xk9POY/akxicXrrHF5804PVqnXlUBUYN4Tk=
|
github.com/fclairamb/ftpserverlib v0.11.1-0.20201224162151-6345897942b7 h1:k7T/6Ik8zVR3TMrCPKxqahbbGKaIpS5pYqb3bXlc40k=
|
||||||
github.com/fclairamb/ftpserverlib v0.11.0/go.mod h1:lCDfM4WqDDh/wlVMZP185tEH93I0t5gsY2/lKfrLl3o=
|
github.com/fclairamb/ftpserverlib v0.11.1-0.20201224162151-6345897942b7/go.mod h1:X6sAMSYtN0YDPu+nHfyE9dsKPUOrEZ8O5EMgt1xvPwk=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||||
|
|
|
@ -1326,7 +1326,7 @@ components:
|
||||||
command:
|
command:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
description: SSH command or WebDAV method
|
description: SSH/FTP command or WebDAV method
|
||||||
last_activity:
|
last_activity:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
|
|
@ -42,7 +42,7 @@ type Binding struct {
|
||||||
Address string `json:"address" mapstructure:"address"`
|
Address string `json:"address" mapstructure:"address"`
|
||||||
// The port used for serving requests
|
// The port used for serving requests
|
||||||
Port int `json:"port" mapstructure:"port"`
|
Port int `json:"port" mapstructure:"port"`
|
||||||
// apply the proxy configuration, if any, for this binding
|
// Apply the proxy configuration, if any, for this binding
|
||||||
ApplyProxyConfig bool `json:"apply_proxy_config" mapstructure:"apply_proxy_config"`
|
ApplyProxyConfig bool `json:"apply_proxy_config" mapstructure:"apply_proxy_config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,10 @@
|
||||||
"start": 50000,
|
"start": 50000,
|
||||||
"end": 50100
|
"end": 50100
|
||||||
},
|
},
|
||||||
|
"disable_active_mode": false,
|
||||||
|
"enable_site": false,
|
||||||
|
"hash_support": 0,
|
||||||
|
"combine_support": 0,
|
||||||
"certificate_file": "",
|
"certificate_file": "",
|
||||||
"certificate_key_file": ""
|
"certificate_key_file": ""
|
||||||
},
|
},
|
||||||
|
|
|
@ -752,6 +752,9 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
err = os.RemoveAll(u.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
|
@ -934,6 +937,9 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user4)
|
err = dataprovider.DeleteUser(user4)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.RemoveAll(u.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRecoverer(t *testing.T) {
|
func TestRecoverer(t *testing.T) {
|
||||||
|
|
|
@ -149,7 +149,8 @@ func TestMain(m *testing.M) {
|
||||||
Port: 9022,
|
Port: 9022,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
sftpdConf.HostKeys = []string{filepath.Join(os.TempDir(), "id_ecdsa")}
|
hostKeyPath := filepath.Join(os.TempDir(), "id_ecdsa")
|
||||||
|
sftpdConf.HostKeys = []string{hostKeyPath}
|
||||||
|
|
||||||
webDavConf := config.GetWebDAVDConfig()
|
webDavConf := config.GetWebDAVDConfig()
|
||||||
webDavConf.Bindings = []webdavd.Binding{
|
webDavConf.Bindings = []webdavd.Binding{
|
||||||
|
@ -217,6 +218,8 @@ func TestMain(m *testing.M) {
|
||||||
os.Remove(postConnectPath)
|
os.Remove(postConnectPath)
|
||||||
os.Remove(certPath)
|
os.Remove(certPath)
|
||||||
os.Remove(keyPath)
|
os.Remove(keyPath)
|
||||||
|
os.Remove(hostKeyPath)
|
||||||
|
os.Remove(hostKeyPath + ".pub")
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue