diff --git a/client_handler.go b/client_handler.go index 3bd89edc..cb5d9d38 100644 --- a/client_handler.go +++ b/client_handler.go @@ -12,6 +12,29 @@ import ( "github.com/fclairamb/ftpserverlib/log" ) +// HASHAlgo is the enumerable that represents the supported HASH algorithms +type HASHAlgo int + +// Supported hash algorithms +const ( + HASHAlgoCRC32 HASHAlgo = iota + HASHAlgoMD5 + HASHAlgoSHA1 + HASHAlgoSHA256 + HASHAlgoSHA512 +) + +func getHashMapping() map[string]HASHAlgo { + mapping := make(map[string]HASHAlgo) + mapping["CRC32"] = HASHAlgoCRC32 + mapping["MD5"] = HASHAlgoMD5 + mapping["SHA-1"] = HASHAlgoSHA1 + mapping["SHA-256"] = HASHAlgoSHA256 + mapping["SHA-512"] = HASHAlgoSHA512 + + return mapping +} + type openTransferError struct { err string } @@ -22,38 +45,40 @@ func (e *openTransferError) Error() string { // nolint: maligned type clientHandler struct { - id uint32 // ID of the client - server *FtpServer // Server on which the connection was accepted - driver ClientDriver // Client handling driver - conn net.Conn // TCP connection - writer *bufio.Writer // Writer on the TCP connection - reader *bufio.Reader // Reader on the TCP connection - user string // Authenticated user - path string // Current path - clnt string // Identified client - command string // Command received on the connection - param string // Param of the FTP command - connectedAt time.Time // Date of connection - ctxRnfr string // Rename from - ctxRest int64 // Restart point - debug bool // Show debugging info on the server side - transfer transferHandler // Transfer connection (only passive is implemented at this stage) - transferTLS bool // Use TLS for transfer connection - controlTLS bool // Use TLS for control connection - logger log.Logger // Client handler logging + id uint32 // ID of the client + server *FtpServer // Server on which the connection was accepted + driver ClientDriver // Client handling driver + conn net.Conn // TCP connection + writer *bufio.Writer // Writer on the TCP connection + reader *bufio.Reader // Reader on the TCP connection + user string // Authenticated user + path string // Current path + clnt string // Identified client + command string // Command received on the connection + param string // Param of the FTP command + connectedAt time.Time // Date of connection + ctxRnfr string // Rename from + ctxRest int64 // Restart point + debug bool // Show debugging info on the server side + transfer transferHandler // Transfer connection (only passive is implemented at this stage) + transferTLS bool // Use TLS for transfer connection + controlTLS bool // Use TLS for control connection + selectedHashAlgo HASHAlgo // algorithm used when we receive the HASH command + logger log.Logger // Client handler logging } // newClientHandler initializes a client handler when someone connects func (server *FtpServer) newClientHandler(connection net.Conn, id uint32) *clientHandler { p := &clientHandler{ - server: server, - conn: connection, - id: id, - writer: bufio.NewWriter(connection), - reader: bufio.NewReader(connection), - connectedAt: time.Now().UTC(), - path: "/", - logger: server.Logger.With("clientId", id), + server: server, + conn: connection, + id: id, + writer: bufio.NewWriter(connection), + reader: bufio.NewReader(connection), + connectedAt: time.Now().UTC(), + path: "/", + selectedHashAlgo: HASHAlgoSHA256, + logger: server.Logger.With("clientId", id), } return p diff --git a/consts.go b/consts.go index ffffd250..a653e2a2 100644 --- a/consts.go +++ b/consts.go @@ -49,4 +49,6 @@ const ( StatusCommandNotImplemented = 502 // RFC 959, 4.2.1 StatusNotLoggedIn = 530 // RFC 959, 4.2.1 StatusActionNotTaken = 550 // RFC 959, 4.2.1 + StatusActionAborted = 552 // RFC 959, 4.2.1 + StatusActionNotTakenNoFile = 553 // RFC 959, 4.2.1 ) diff --git a/driver.go b/driver.go index 07c09833..c6613317 100644 --- a/driver.go +++ b/driver.go @@ -91,6 +91,12 @@ type ClientDriverExtensionRemoveDir interface { RemoveDir(name string) error } +// ClientDriverExtensionHasher is an extension to implement if you want to handle file digests +// yourself. You have to set EnableHASH to true for this extension to be called +type ClientDriverExtensionHasher interface { + ComputeHash(name string, algo HASHAlgo, startOffset, endOffset int64) (string, error) +} + // ClientContext is implemented on the server side to provide some access to few data around the client type ClientContext interface { // Path provides the path of the current connection @@ -173,4 +179,5 @@ type Settings struct { DisableLISTArgs bool // Disable ls like options (-a,-la etc.) for directory listing DisableSite bool // Disable SITE command DisableActiveMode bool // Disable Active FTP + EnableHASH bool // Enable support for calculating hash value of files } diff --git a/handle_files.go b/handle_files.go index 85eb4331..4b205b4e 100644 --- a/handle_files.go +++ b/handle_files.go @@ -2,7 +2,14 @@ package ftpserver import ( + "crypto/md5" //nolint:gosec + "crypto/sha1" //nolint:gosec + "crypto/sha256" + "crypto/sha512" + "encoding/hex" "fmt" + "hash" + "hash/crc32" "io" "net" "os" @@ -401,3 +408,147 @@ func (c *clientHandler) handleMFMT() error { return nil } + +func (c *clientHandler) handleHASH() error { + return c.handleGenericHash(c.selectedHashAlgo, false) +} + +func (c *clientHandler) handleCRC32() error { + return c.handleGenericHash(HASHAlgoCRC32, true) +} + +func (c *clientHandler) handleMD5() error { + return c.handleGenericHash(HASHAlgoMD5, true) +} + +func (c *clientHandler) handleSHA1() error { + return c.handleGenericHash(HASHAlgoSHA1, true) +} + +func (c *clientHandler) handleSHA256() error { + return c.handleGenericHash(HASHAlgoSHA256, true) +} + +func (c *clientHandler) handleSHA512() error { + return c.handleGenericHash(HASHAlgoSHA512, true) +} + +func (c *clientHandler) handleGenericHash(algo HASHAlgo, isCustomMode bool) error { + args := strings.SplitN(c.param, " ", 3) + info, err := c.driver.Stat(args[0]) + + if err != nil { + c.writeMessage(StatusActionNotTaken, fmt.Sprintf("%v: %v", c.param, err)) + return nil + } + + if !info.Mode().IsRegular() { + c.writeMessage(StatusActionNotTakenNoFile, fmt.Sprintf("%v is not a regular file", c.param)) + return nil + } + + start := int64(0) + end := info.Size() + + if isCustomMode { + // for custom command the range can be specified in this way: + // XSHA1 + if len(args) > 1 { + start, err = strconv.ParseInt(args[1], 10, 64) + if err != nil { + c.writeMessage(StatusSyntaxErrorParameters, fmt.Sprintf("invalid start offset %v: %v", args[1], err)) + return nil + } + } + + if len(args) > 2 { + end, err = strconv.ParseInt(args[2], 10, 64) + if err != nil { + c.writeMessage(StatusSyntaxErrorParameters, fmt.Sprintf("invalid end offset %v2: %v", args[2], err)) + return nil + } + } + } + // to support partial hash also for the HASH command we should implement RANG too, + // but this apply also to uploads/downloads and so complicat the things, we'll add + // this support in future improvements + var result string + if hasher, ok := c.driver.(ClientDriverExtensionHasher); ok { + result, err = hasher.ComputeHash(c.absPath(args[0]), algo, start, end) + } else { + result, err = c.computeHashForFile(c.absPath(args[0]), algo, start, end) + } + + if err != nil { + c.writeMessage(StatusActionNotTaken, fmt.Sprintf("%v: %v", args[0], err)) + return nil + } + + hashMapping := getHashMapping() + hashName := "" + + for k, v := range hashMapping { + if v == algo { + hashName = k + } + } + + firstLine := fmt.Sprintf("Computing %v digest", hashName) + + if isCustomMode { + c.writeMessage(StatusFileOK, fmt.Sprintf("%v\r\n%v", firstLine, result)) + return nil + } + + response := fmt.Sprintf("%v\r\n%v %v-%v %v %v", firstLine, hashName, start, end, result, args[0]) + c.writeMessage(StatusFileStatus, response) + + return nil +} + +func (c *clientHandler) computeHashForFile(filePath string, algo HASHAlgo, start, end int64) (string, error) { + var h hash.Hash + var file FileTransfer + var err error + + switch algo { + case HASHAlgoCRC32: + h = crc32.NewIEEE() + case HASHAlgoMD5: + h = md5.New() //nolint:gosec + case HASHAlgoSHA1: + h = sha1.New() //nolint:gosec + case HASHAlgoSHA256: + h = sha256.New() + case HASHAlgoSHA512: + h = sha512.New() + default: + return "", errUnknowHash + } + + if fileTransfer, ok := c.driver.(ClientDriverExtentionFileTransfer); ok { + file, err = fileTransfer.GetHandle(filePath, os.O_RDONLY, start) + } else { + file, err = c.driver.OpenFile(filePath, os.O_RDONLY, os.ModePerm) + } + + if err != nil { + return "", err + } + + if start > 0 { + _, err = file.Seek(start, io.SeekStart) + if err != nil { + return "", err + } + } + + _, err = io.CopyN(h, file, end-start) + defer file.Close() //nolint:errcheck // we ignore close error here + + if err != nil && err != io.EOF { + return "", err + } + + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/handle_files_test.go b/handle_files_test.go index 0d8c7ecd..61810525 100644 --- a/handle_files_test.go +++ b/handle_files_test.go @@ -1,7 +1,11 @@ package ftpserver import ( + "fmt" + "io/ioutil" + "os" "regexp" + "strings" "testing" "github.com/secsy/goftp" @@ -308,3 +312,132 @@ func TestSTATFile(t *testing.T) { require.NoError(t, err) require.Equal(t, StatusFileActionNotTaken, rc) } + +func TestHASHCommand(t *testing.T) { + s := NewTestServerWithDriver( + t, + &TestServerDriver{ + Debug: true, + Settings: &Settings{ + EnableHASH: true, + }, + }, + ) + conf := goftp.Config{ + User: authUser, + Password: authPass, + } + + c, err := goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") + + defer func() { panicOnError(c.Close()) }() + + dir, err := c.Mkdir("testdir") + require.NoError(t, err) + + tempFile, err := ioutil.TempFile("", "ftpserver") + require.NoError(t, err) + err = ioutil.WriteFile(tempFile.Name(), []byte("sample data with know checksum/hash\n"), os.ModePerm) + require.NoError(t, err) + + crc32Sum := "21b0f382" + sha256Hash := "ceee704dd96e2b8c2ceca59c4c697bc01123fb9e66a1a3ac34dbdd2d6da9659b" + + ftpUpload(t, c, tempFile, "file.txt") + + var raw goftp.RawConn + + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") + + // ask hash for a directory + rc, _, err := raw.SendCommand(fmt.Sprintf("XSHA256 %v", dir)) + require.NoError(t, err) + require.Equal(t, StatusActionNotTakenNoFile, rc) + + // test the HASH command + rc, message, err := raw.SendCommand("HASH file.txt") + require.NoError(t, err) + require.Equal(t, StatusFileStatus, rc) + require.True(t, strings.HasSuffix(message, fmt.Sprintf("SHA-256 0-36 %v file.txt", sha256Hash))) + + // change algo and request the hash again + rc, message, err = raw.SendCommand("OPTS HASH CRC32") + require.NoError(t, err) + require.Equal(t, StatusOK, rc) + require.Equal(t, "CRC32", message) + + rc, message, err = raw.SendCommand("HASH file.txt") + require.NoError(t, err) + require.Equal(t, StatusFileStatus, rc) + require.True(t, strings.HasSuffix(message, fmt.Sprintf("CRC32 0-36 %v file.txt", crc32Sum))) +} + +func TestCustomHASHCommands(t *testing.T) { + s := NewTestServerWithDriver( + t, + &TestServerDriver{ + Debug: true, + Settings: &Settings{ + EnableHASH: true, + }, + }, + ) + conf := goftp.Config{ + User: authUser, + Password: authPass, + } + + c, err := goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") + + defer func() { panicOnError(c.Close()) }() + + tempFile, err := ioutil.TempFile("", "ftpserver") + require.NoError(t, err) + err = ioutil.WriteFile(tempFile.Name(), []byte("sample data with know checksum/hash\n"), os.ModePerm) + require.NoError(t, err) + + ftpUpload(t, c, tempFile, "file.txt") + + var raw goftp.RawConn + + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") + + customCommands := make(map[string]string) + customCommands["XCRC"] = "21b0f382" + customCommands["MD5"] = "6905e38270e1797e68f69026bfbef131" + customCommands["XMD5"] = "6905e38270e1797e68f69026bfbef131" + customCommands["XSHA"] = "0f11c4103a2573b14edd4733984729f2380d99ed" + customCommands["XSHA1"] = "0f11c4103a2573b14edd4733984729f2380d99ed" + customCommands["XSHA256"] = "ceee704dd96e2b8c2ceca59c4c697bc01123fb9e66a1a3ac34dbdd2d6da9659b" + customCommands["XSHA512"] = "4f95c20e4d030cbc43b1e139a0fe11c5e0e5e520cf3265bae852ae212b1c7cdb02c2fea5ba038cbf3202af8cdf313579fbe344d47919c288c16d6dd671e9db63" //nolint:lll + + var rc int + var message string + + for cmd, expected := range customCommands { + rc, message, err = raw.SendCommand(fmt.Sprintf("%v file.txt", cmd)) + require.NoError(t, err) + require.Equal(t, StatusFileOK, rc) + require.True(t, strings.HasSuffix(message, expected)) + } + + // now a partial hash + rc, message, err = raw.SendCommand("XSHA256 file.txt 7 11") + require.NoError(t, err) + require.Equal(t, StatusFileOK, rc) + require.True(t, strings.HasSuffix(message, "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7")) + + // invalid start + rc, _, err = raw.SendCommand("XSHA256 file.txt a 11") + require.NoError(t, err) + require.Equal(t, StatusSyntaxErrorParameters, rc) + + // invalid end + rc, _, err = raw.SendCommand("XSHA256 file.txt 7 a") + require.NoError(t, err) + require.Equal(t, StatusSyntaxErrorParameters, rc) +} diff --git a/handle_misc.go b/handle_misc.go index 6037ee66..97d4dc0e 100644 --- a/handle_misc.go +++ b/handle_misc.go @@ -4,11 +4,14 @@ package ftpserver import ( "bufio" "crypto/tls" + "errors" "fmt" "strings" "time" ) +var errUnknowHash = errors.New("unknown hash algorithm") + func (c *clientHandler) handleAUTH() error { if tlsConfig, err := c.server.driver.GetTLSConfig(); err == nil { c.writeMessage(StatusAuthAccepted, "AUTH command ok. Expecting TLS Negotiation.") @@ -103,10 +106,39 @@ func (c *clientHandler) handleOPTS() error { args := strings.SplitN(c.param, " ", 2) if strings.EqualFold(args[0], "UTF8") { c.writeMessage(StatusOK, "I'm in UTF8 only anyway") - } else { - c.writeMessage(StatusSyntaxErrorNotRecognised, "Don't know this option") + return nil } + if strings.EqualFold(args[0], "HASH") && c.server.settings.EnableHASH { + hashMapping := getHashMapping() + + if len(args) > 1 { + // try to change the current hash algorithm to the requested one + if value, ok := hashMapping[args[1]]; ok { + c.selectedHashAlgo = value + c.writeMessage(StatusOK, args[1]) + } else { + c.writeMessage(StatusSyntaxErrorParameters, "Unknown algorithm, current selection not changed") + } + + return nil + } + // return the current hash algorithm + var currentHash string + + for k, v := range hashMapping { + if v == c.selectedHashAlgo { + currentHash = k + } + } + + c.writeMessage(StatusOK, currentHash) + + return nil + } + + c.writeMessage(StatusSyntaxErrorNotRecognised, "Don't know this option") + return nil } @@ -151,6 +183,26 @@ func (c *clientHandler) handleFEAT() error { features = append(features, "AUTH TLS") } + if c.server.settings.EnableHASH { + var hashLine strings.Builder + + nonStandardHashImpl := []string{"XCRC", "MD5", "XMD5", "XSHA", "XSHA1", "XSHA256", "XSHA512"} + hashMapping := getHashMapping() + + for k, v := range hashMapping { + hashLine.WriteString(k) + + if v == c.selectedHashAlgo { + hashLine.WriteString("*") + } + + hashLine.WriteString(";") + } + + features = append(features, hashLine.String()) + features = append(features, nonStandardHashImpl...) + } + for _, f := range features { c.writeLine(" " + f) } diff --git a/handle_misc_test.go b/handle_misc_test.go index 9fa17279..88fbf19f 100644 --- a/handle_misc_test.go +++ b/handle_misc_test.go @@ -7,144 +7,212 @@ import ( "time" "github.com/secsy/goftp" + "github.com/stretchr/testify/require" ) func TestSiteCommand(t *testing.T) { s := NewTestServer(t, true) conf := goftp.Config{ - User: "test", - Password: "test", + User: authUser, + Password: authPass, } var err error - var c *goftp.Client - if c, err = goftp.DialConfig(conf, s.Addr()); err != nil { - t.Fatal("Couldn't connect", err) - } + c, err = goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") defer func() { panicOnError(c.Close()) }() var raw goftp.RawConn - if raw, err = c.OpenRawConn(); err != nil { - t.Fatal("Couldn't open raw connection") - } + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") - if rc, response, err := raw.SendCommand("SITE HELP"); err != nil { - t.Fatal("Command not accepted", err) - } else { - if rc != 500 { - t.Fatal("Are we supporting it now ?", rc) - } - if response != "Not understood SITE subcommand" { - t.Fatal("Are we supporting it now ?", response) - } - } + rc, response, err := raw.SendCommand("SITE HELP") + require.NoError(t, err) + require.Equal(t, StatusSyntaxErrorNotRecognised, rc, "Are we supporting it now ?") + require.Equal(t, "Not understood SITE subcommand", response, "Are we supporting it now ?") } // florent(2018-01-14): #58: IDLE timeout: Testing timeout +// drakkan(2020-12-12): idle time is broken if you set timeout to 1 minute +// and a transfer requires more than 1 minutes any command issued at the transfer end +// will timeout. I handle idle timeout myself in SFTPGo but you could be +// interested to fix this bug func TestIdleTimeout(t *testing.T) { s := NewTestServerWithDriver(t, &TestServerDriver{Debug: true, Settings: &Settings{IdleTimeout: 2}}) conf := goftp.Config{ - User: "test", - Password: "test", + User: authUser, + Password: authPass, } var err error - var c *goftp.Client - if c, err = goftp.DialConfig(conf, s.Addr()); err != nil { - t.Fatal("Couldn't connect", err) - } + c, err = goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") defer func() { panicOnError(c.Close()) }() var raw goftp.RawConn - if raw, err = c.OpenRawConn(); err != nil { - t.Fatal("Couldn't open raw connection") - } + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") time.Sleep(time.Second * 1) // < 2s : OK - if rc, _, err := raw.SendCommand("NOOP"); err != nil || rc != 200 { - t.Fatal("Command not accepted", rc, err) - } + rc, _, err := raw.SendCommand("NOOP") + require.NoError(t, err) + require.Equal(t, StatusOK, rc) time.Sleep(time.Second * 3) // > 2s : Timeout - if rc, _, err := raw.SendCommand("NOOP"); err != nil || rc != 421 { - t.Fatal("Command should have failed !") - } + rc, _, err = raw.SendCommand("NOOP") + require.NoError(t, err) + require.Equal(t, StatusServiceNotAvailable, rc) } func TestStat(t *testing.T) { s := NewTestServer(t, true) conf := goftp.Config{ - User: "test", - Password: "test", + User: authUser, + Password: authPass, } var err error - var c *goftp.Client - if c, err = goftp.DialConfig(conf, s.Addr()); err != nil { - t.Fatal("Couldn't connect", err) - } + c, err = goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") defer func() { panicOnError(c.Close()) }() var raw goftp.RawConn - if raw, err = c.OpenRawConn(); err != nil { - t.Fatal("Couldn't open raw connection") - } + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") - if rc, str, err := raw.SendCommand("STAT"); err != nil || rc != 213 { - t.Fatal("Wrong STAT response", err, rc) - } else { - count := strings.Count(str, "\n") - if count < 4 { - t.Fatal("More lines expected", count) - } - if str[0] == ' ' { - t.Fatal("Isn't that a mistake ?") - } - } + rc, str, err := raw.SendCommand("STAT") + require.NoError(t, err) + require.Equal(t, StatusFileStatus, rc) + + count := strings.Count(str, "\n") + require.GreaterOrEqual(t, count, 4) + require.NotEqual(t, ' ', str[0]) } func TestCLNT(t *testing.T) { s := NewTestServer(t, true) conf := goftp.Config{ - User: "test", - Password: "test", + User: authUser, + Password: authPass, } var err error - var c *goftp.Client - if c, err = goftp.DialConfig(conf, s.Addr()); err != nil { - t.Fatal("Couldn't connect", err) + c, err = goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") + + defer func() { panicOnError(c.Close()) }() + + var raw goftp.RawConn + + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") + + rc, _, err := raw.SendCommand("CLNT NcFTP 3.2.6 macosx10.15") + require.NoError(t, err) + require.Equal(t, StatusOK, rc) +} + +func TestOPTSUTF8(t *testing.T) { + s := NewTestServer(t, true) + + conf := goftp.Config{ + User: authUser, + Password: authPass, } + var err error + var c *goftp.Client + + c, err = goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") + defer func() { panicOnError(c.Close()) }() var raw goftp.RawConn - if raw, err = c.OpenRawConn(); err != nil { - t.Fatal("Couldn't open raw connection") + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") + + for _, cmd := range []string{"OPTS UTF8", "OPTS UTF8 ON"} { + rc, message, err := raw.SendCommand(cmd) + require.NoError(t, err) + require.Equal(t, StatusOK, rc) + require.Equal(t, "I'm in UTF8 only anyway", message) } +} - if rc, _, err := raw.SendCommand("CLNT NcFTP 3.2.6 macosx10.15"); err != nil || rc != 200 { - t.Fatal("Wrong CLNT response", err, rc) +func TestOPTSHASH(t *testing.T) { + s := NewTestServerWithDriver( + t, + &TestServerDriver{ + Debug: true, + Settings: &Settings{ + EnableHASH: true, + }, + }, + ) + conf := goftp.Config{ + User: authUser, + Password: authPass, } + + var err error + var c *goftp.Client + + c, err = goftp.DialConfig(conf, s.Addr()) + require.NoError(t, err, "Couldn't connect") + + defer func() { panicOnError(c.Close()) }() + + var raw goftp.RawConn + + raw, err = c.OpenRawConn() + require.NoError(t, err, "Couldn't open raw connection") + + rc, message, err := raw.SendCommand("OPTS HASH") + require.NoError(t, err) + require.Equal(t, StatusOK, rc) + require.Equal(t, "SHA-256", message) + + rc, message, err = raw.SendCommand("OPTS HASH MD5") + require.NoError(t, err) + require.Equal(t, StatusOK, rc) + require.Equal(t, "MD5", message) + + rc, message, err = raw.SendCommand("OPTS HASH CRC-37") + require.NoError(t, err) + require.Equal(t, StatusSyntaxErrorParameters, rc) + require.Equal(t, "Unknown algorithm, current selection not changed", message) + + rc, message, err = raw.SendCommand("OPTS HASH") + require.NoError(t, err) + require.Equal(t, StatusOK, rc) + require.Equal(t, "MD5", message) + + // now disable hash support + s.settings.EnableHASH = false + + rc, _, err = raw.SendCommand("OPTS HASH") + require.NoError(t, err) + require.Equal(t, StatusSyntaxErrorNotRecognised, rc) } diff --git a/server.go b/server.go index ab9f1aa8..81fc6181 100644 --- a/server.go +++ b/server.go @@ -42,19 +42,27 @@ var commandsMap = map[string]*CommandDescription{ "QUIT": {Fn: (*clientHandler).handleQUIT, Open: true}, // File access - "SIZE": {Fn: (*clientHandler).handleSIZE}, - "STAT": {Fn: (*clientHandler).handleSTAT}, - "MDTM": {Fn: (*clientHandler).handleMDTM}, - "MFMT": {Fn: (*clientHandler).handleMFMT}, - "RETR": {Fn: (*clientHandler).handleRETR}, - "STOR": {Fn: (*clientHandler).handleSTOR}, - "APPE": {Fn: (*clientHandler).handleAPPE}, - "DELE": {Fn: (*clientHandler).handleDELE}, - "RNFR": {Fn: (*clientHandler).handleRNFR}, - "RNTO": {Fn: (*clientHandler).handleRNTO}, - "ALLO": {Fn: (*clientHandler).handleALLO}, - "REST": {Fn: (*clientHandler).handleREST}, - "SITE": {Fn: (*clientHandler).handleSITE}, + "SIZE": {Fn: (*clientHandler).handleSIZE}, + "STAT": {Fn: (*clientHandler).handleSTAT}, + "MDTM": {Fn: (*clientHandler).handleMDTM}, + "MFMT": {Fn: (*clientHandler).handleMFMT}, + "RETR": {Fn: (*clientHandler).handleRETR}, + "STOR": {Fn: (*clientHandler).handleSTOR}, + "APPE": {Fn: (*clientHandler).handleAPPE}, + "DELE": {Fn: (*clientHandler).handleDELE}, + "RNFR": {Fn: (*clientHandler).handleRNFR}, + "RNTO": {Fn: (*clientHandler).handleRNTO}, + "ALLO": {Fn: (*clientHandler).handleALLO}, + "REST": {Fn: (*clientHandler).handleREST}, + "SITE": {Fn: (*clientHandler).handleSITE}, + "HASH": {Fn: (*clientHandler).handleHASH}, + "XCRC": {Fn: (*clientHandler).handleCRC32}, + "MD5": {Fn: (*clientHandler).handleMD5}, + "XMD5": {Fn: (*clientHandler).handleMD5}, + "XSHA": {Fn: (*clientHandler).handleSHA1}, + "XSHA1": {Fn: (*clientHandler).handleSHA1}, + "XSHA256": {Fn: (*clientHandler).handleSHA256}, + "XSHA512": {Fn: (*clientHandler).handleSHA512}, // Directory handling "CWD": {Fn: (*clientHandler).handleCWD},