Skip to content

Commit

Permalink
feat(deflate): Non working deflate transfer mode
Browse files Browse the repository at this point in the history
  • Loading branch information
fclairamb committed Jun 2, 2024
1 parent 117fcb6 commit 8ebd068
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 11 deletions.
54 changes: 54 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,38 @@ 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 {
errWriter := c.Writer.Flush()

if errWriter != nil {
return errWriter
}

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
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
88 changes: 79 additions & 9 deletions transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,20 @@ 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
}

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,6 +138,11 @@ func ftpDownloadAndHashWithRawConnection(t *testing.T, raw goftp.RawConn, fileNa
dataConn, err := dcGetter()
req.NoError(err)

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

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

Expand All @@ -143,15 +156,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 +184,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 +563,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 +599,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 +954,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 +966,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 +1006,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 +1258,46 @@ func getPortFromPASVResponse(t *testing.T, resp string) int {

return port
}

func TestTransferModeDeflate(t *testing.T) {
s := NewTestServer(t, false)
conf := goftp.Config{
User: authUser,
Password: authPass,
}
client, err := goftp.DialConfig(conf, s.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)

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)

remoteHash := ftpDownloadAndHashWithRawConnection(t, raw, "file.txt", &ftpDownloadOptions{deflateMode: true})
localHash := hashFile(t, file)
require.Equal(t, localHash, remoteHash)
}

0 comments on commit 8ebd068

Please sign in to comment.