Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(deflate): Non working deflate transfer mode #461

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions client_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ftpserver

import (
"bufio"
"compress/flate"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -34,6 +35,14 @@ const (
TransferTypeBinary
)

// TransferMode is the enumerable that represents the transfer mode (stream, block, compressed, deflate)
type TransferMode int8

const (
TransferModeStream TransferMode = iota // TransferModeStream is the standard mode
TransferModeDeflate // TransferModeDeflate is the deflate mode
)

// DataChannel is the enumerable that represents the data channel (active or passive)
type DataChannel int8

Expand Down Expand Up @@ -99,6 +108,7 @@ type clientHandler struct {
selectedHashAlgo HASHAlgo // algorithm used when we receive the HASH command
logger log.Logger // Client handler logging
currentTransferType TransferType // current transfer type
transferMode TransferMode // Transfer mode (stream, block, compressed)
transferWg sync.WaitGroup // wait group for command that open a transfer connection
transferMu sync.Mutex // this mutex will protect the transfer parameters
transfer transferHandler // Transfer connection (passive or active)s
Expand Down Expand Up @@ -663,6 +673,15 @@ func (c *clientHandler) TransferOpen(info string) (net.Conn, error) {
return nil, err
}

if c.transferMode == TransferModeDeflate {
conn, err = newDeflateConn(conn, c.server.settings.DeflateCompressionLevel)
if err != nil {
c.writeMessage(StatusActionNotTaken, fmt.Sprintf("Could not switch to deflate mode: %v", err))

return nil, fmt.Errorf("could not switch to deflate mode: %w", err)
}
}

c.isTransferOpen = true
c.transfer.SetInfo(info)

Expand Down Expand Up @@ -788,3 +807,36 @@ func getMessageLines(message string) []string {

return lines
}

type deflateConn struct {
net.Conn
io.Reader
*flate.Writer
}

func (c *deflateConn) Read(p []byte) (int, error) {
return c.Reader.Read(p)
}

func (c *deflateConn) Write(p []byte) (int, error) {
return c.Writer.Write(p)
}

func (c *deflateConn) Close() error {
if err := c.Writer.Close(); err != nil {
return fmt.Errorf("could not close deflate writer: %w", err)
}

return c.Conn.Close()
}

func newDeflateConn(conn net.Conn, compressionLevel int) (net.Conn, error) {
reader := flate.NewReader(conn)
writer, err := flate.NewWriter(conn, compressionLevel)

if err != nil {
return nil, fmt.Errorf("could not create deflate writer: %w", err)
}

return &deflateConn{Conn: conn, Reader: reader, Writer: writer}, nil
}
1 change: 1 addition & 0 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ type Settings struct {
DisableSTAT bool // Disable Server STATUS, STAT on files and directories will still work
DisableSYST bool // Disable SYST
EnableCOMB bool // Enable COMB support
DeflateCompressionLevel int // Deflate compression level (1-9)
DefaultTransferType TransferType // Transfer type to use if the client don't send the TYPE command
// ActiveConnectionsCheck defines the security requirements for active connections
ActiveConnectionsCheck DataConnectionRequirement
Expand Down
7 changes: 4 additions & 3 deletions driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ func (driver *TestServerDriver) Init() {
}

{
dir, _ := os.MkdirTemp("", "example")
if err := os.MkdirAll(dir, 0o750); err != nil {
driver.serverDir, _ = os.MkdirTemp("", "example")
if err := os.MkdirAll(driver.serverDir, 0o750); err != nil {
panic(err)
}

driver.fs = afero.NewBasePathFs(afero.NewOsFs(), dir)
driver.fs = afero.NewBasePathFs(afero.NewOsFs(), driver.serverDir)
}
}

Expand Down Expand Up @@ -126,6 +126,7 @@ type TestServerDriver struct {
CloseOnConnect bool // disconnect the client as soon as it connects

Settings *Settings // Settings
serverDir string
fs afero.Fs
clientMU sync.Mutex
Clients []ClientContext
Expand Down
9 changes: 7 additions & 2 deletions handle_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,14 @@ func (c *clientHandler) handleTYPE(param string) error {
}

func (c *clientHandler) handleMODE(param string) error {
if param == "S" {
switch param {
case "S":
c.transferMode = TransferModeStream
c.writeMessage(StatusOK, "Using stream mode")
} else {
case "Z":
c.transferMode = TransferModeDeflate
c.writeMessage(StatusOK, "Using deflate mode")
default:
c.writeMessage(StatusNotImplementedParam, "Unsupported mode")
}

Expand Down
4 changes: 4 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ func (server *FtpServer) loadSettings() error {
settings.Banner = "ftpserver - golang FTP server"
}

if settings.DeflateCompressionLevel == 0 {
settings.DeflateCompressionLevel = 5
}

server.settings = settings

return nil
Expand Down
3 changes: 3 additions & 0 deletions transfer_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ func (c *clientHandler) handlePORT(param string) error {
return nil
}

// activeTransferHandler implements the transferHandler interface
var _ transferHandler = (*activeTransferHandler)(nil)

// Active connection
type activeTransferHandler struct {
raddr *net.TCPAddr // Remote address of the client
Expand Down
3 changes: 3 additions & 0 deletions transfer_pasv.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ type transferHandler interface {
GetInfo() string
}

// activeTransferHandler implements the transferHandler interface
var _ transferHandler = (*passiveTransferHandler)(nil)

// Passive connection
type passiveTransferHandler struct {
listener net.Listener // TCP or SSL Listener
Expand Down
120 changes: 110 additions & 10 deletions transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"math/rand"
"net"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -114,12 +115,21 @@ func ftpDownloadAndHash(t *testing.T, ftp *goftp.Client, filename string) string
return hex.EncodeToString(hasher.Sum(nil))
}

func ftpDownloadAndHashWithRawConnection(t *testing.T, raw goftp.RawConn, fileName string) string {
type ftpDownloadOptions struct {
deflateMode bool
otherWriter io.Writer
}

func ftpDownloadAndHashWithRawConnection(t *testing.T, raw goftp.RawConn, fileName string, options *ftpDownloadOptions) string {
t.Helper()

req := require.New(t)
hasher := sha256.New()

if options == nil {
options = &ftpDownloadOptions{}
}

dcGetter, err := raw.PrepareDataConn()
req.NoError(err)

Expand All @@ -130,7 +140,18 @@ func ftpDownloadAndHashWithRawConnection(t *testing.T, raw goftp.RawConn, fileNa
dataConn, err := dcGetter()
req.NoError(err)

_, err = io.Copy(hasher, dataConn)
if options.deflateMode {
dataConn, err = newDeflateConn(dataConn, 5)
req.NoError(err)
}

var writer io.Writer = hasher

if options.otherWriter != nil {
writer = io.MultiWriter(writer, options.otherWriter)
}

_, err = io.Copy(writer, dataConn)
req.NoError(err)

err = dataConn.Close()
Expand All @@ -143,15 +164,24 @@ func ftpDownloadAndHashWithRawConnection(t *testing.T, raw goftp.RawConn, fileNa
return hex.EncodeToString(hasher.Sum(nil))
}

func ftpUploadWithRawConnection(t *testing.T, raw goftp.RawConn, file io.Reader, fileName string, appendFile bool) {
type ftpUploadOptions struct {
appendFile bool
deflateMode bool
}

func ftpUploadWithRawConnection(t *testing.T, raw goftp.RawConn, file io.Reader, fileName string, options *ftpUploadOptions) {
t.Helper()

req := require.New(t)
dcGetter, err := raw.PrepareDataConn()
req.NoError(err)

if options == nil {
options = &ftpUploadOptions{}
}

cmd := "STOR"
if appendFile {
if options.appendFile {
cmd = "APPE"
}

Expand All @@ -162,6 +192,11 @@ func ftpUploadWithRawConnection(t *testing.T, raw goftp.RawConn, file io.Reader,
dataConn, err := dcGetter()
req.NoError(err)

if options.deflateMode {
dataConn, err = newDeflateConn(dataConn, 5)
req.NoError(err)
}

_, err = io.Copy(dataConn, file)
req.NoError(err)

Expand Down Expand Up @@ -536,7 +571,7 @@ func TestAPPEExistingFile(t *testing.T) {
_, err = file.Seek(1024, io.SeekStart)
require.NoError(t, err)

ftpUploadWithRawConnection(t, raw, file, fileName, true)
ftpUploadWithRawConnection(t, raw, file, fileName, &ftpUploadOptions{appendFile: true})

info, err := client.Stat(fileName)
require.NoError(t, err)
Expand Down Expand Up @@ -572,7 +607,7 @@ func TestAPPENewFile(t *testing.T) {

fileName := filepath.Base(file.Name())

ftpUploadWithRawConnection(t, raw, file, fileName, true)
ftpUploadWithRawConnection(t, raw, file, fileName, &ftpUploadOptions{appendFile: true})

localHash := hashFile(t, file)
remoteHash := ftpDownloadAndHash(t, client, fileName)
Expand Down Expand Up @@ -927,7 +962,7 @@ func TestASCIITransfers(t *testing.T) {
_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)

ftpUploadWithRawConnection(t, raw, file, "file.txt", false)
ftpUploadWithRawConnection(t, raw, file, "file.txt", nil)

files, err := client.ReadDir("/")
require.NoError(t, err)
Expand All @@ -939,7 +974,7 @@ func TestASCIITransfers(t *testing.T) {
require.Equal(t, int64(len(contents)), files[0].Size())
}

remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.txt")
remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.txt", nil)
localHash := hashFile(t, file)
require.Equal(t, localHash, remoteHash)
}
Expand Down Expand Up @@ -979,9 +1014,9 @@ func TestASCIITransfersInvalidFiles(t *testing.T) {
require.NoError(t, err)
require.Equal(t, StatusOK, rc, response)

ftpUploadWithRawConnection(t, raw, file, "file.bin", false)
ftpUploadWithRawConnection(t, raw, file, "file.bin", nil)

remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.bin")
remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.bin", nil)
require.Equal(t, localHash, remoteHash)
}

Expand Down Expand Up @@ -1231,3 +1266,68 @@ func getPortFromPASVResponse(t *testing.T, resp string) int {

return port
}

func TestTransferModeDeflate(t *testing.T) {
driver := &TestServerDriver{Debug: true}
server := NewTestServerWithTestDriver(t, driver)

conf := goftp.Config{
User: authUser,
Password: authPass,
}
client, err := goftp.DialConfig(conf, server.Addr())
require.NoError(t, err, "Couldn't connect")

defer func() { require.NoError(t, client.Close()) }()

raw, err := client.OpenRawConn()
require.NoError(t, err)

defer func() { require.NoError(t, raw.Close()) }()

file, err := os.CreateTemp("", "ftpserver")
require.NoError(t, err)

contents := []byte("line1\r\n\r\nline3\r\n,line4")
_, err = file.Write(contents)
require.NoError(t, err)
localHash := hashFile(t, file)

defer func() { require.NoError(t, file.Close()) }()

rc, response, err := raw.SendCommand("MODE Z")
require.NoError(t, err)
require.Equal(t, StatusOK, rc, response)

_, err = file.Seek(0, io.SeekStart)
require.NoError(t, err)

ftpUploadWithRawConnection(t, raw, file, "file.txt", &ftpUploadOptions{deflateMode: true})

files, err := client.ReadDir("/")
require.NoError(t, err)
require.Len(t, files, 1)

{ // Hash on server
fp, err := os.Open(path.Join(driver.serverDir, "file.txt"))
require.NoError(t, err)

defer func() { require.NoError(t, fp.Close()) }()

readContents, err := io.ReadAll(fp)
require.NoError(t, err)
require.Equal(t, contents, readContents)
}

{ // Hash on standard connection
writer := bytes.NewBuffer(nil)
remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.txt", &ftpDownloadOptions{otherWriter: writer})
require.Equal(t, string(contents), writer.String())
require.Equal(t, localHash, remoteHash)
}

{ // Hash on deflate connection
remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.txt", &ftpDownloadOptions{deflateMode: true})
require.Equal(t, localHash, remoteHash)
}
}
Loading