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:
Nicola Murino
2020-12-24 18:48:06 +01:00
parent 5b1d8666b3
commit 1dce1eff48
15 changed files with 375 additions and 31 deletions

View File

@@ -2,7 +2,9 @@ package ftpd_test
import (
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"io"
@@ -37,6 +39,7 @@ const (
logSender = "ftpdTesting"
ftpServerAddr = "127.0.0.1:2121"
sftpServerAddr = "127.0.0.1:2122"
ftpSrvAddrTLS = "127.0.0.1:2124" // ftp server with implicit tls
defaultUsername = "test_user_ftp"
defaultPassword = "test_password"
configDir = ".."
@@ -157,6 +160,7 @@ func TestMain(m *testing.M) {
ftpdConf.BannerFile = bannerFileName
ftpdConf.CertificateFile = certPath
ftpdConf.CertificateKeyFile = keyPath
ftpdConf.EnableSite = true
// required to test sftpfs
sftpdConf := config.GetSFTPDConfig()
@@ -165,7 +169,8 @@ func TestMain(m *testing.M) {
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")
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
@@ -205,6 +210,35 @@ func TestMain(m *testing.M) {
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
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()
os.Remove(logFilePath)
os.Remove(bannerFile)
@@ -213,6 +247,8 @@ func TestMain(m *testing.M) {
os.Remove(postConnectPath)
os.Remove(certPath)
os.Remove(keyPath)
os.Remove(hostKeyPath)
os.Remove(hostKeyPath + ".pub")
os.Exit(exitCode)
}
@@ -1578,6 +1614,218 @@ func TestChmod(t *testing.T) {
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 {
_, err := client.CurrentDir()
if err != nil {
@@ -1647,6 +1895,30 @@ func ftpDownloadFile(remoteSourcePath string, localDestPath string, expectedSize
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) {
ftpOptions := []ftp.DialOption{ftp.DialWithTimeout(5 * time.Second)}
if useTLS {
@@ -1662,7 +1934,7 @@ func getFTPClient(user dataprovider.User, useTLS bool) (*ftp.ServerConn, error)
return nil, err
}
pwd := defaultPassword
if len(user.Password) > 0 {
if user.Password != "" {
pwd = user.Password
}
err = client.Login(user.Username, pwd)