Skip to content

Commit

Permalink
add support for calculating the hash value of files (fclairamb#196)
Browse files Browse the repository at this point in the history
* add support for calculating the hash value of files

We implement the following custom FTP commands:

- XCRC (requests CRC32 digest/checksum)
- MD5/XMD5 (requests MD5 digest/checksum)
- XSHA/XSHA1 (requests SHA1 digest/checksum)
- XSHA256 (requests SHA256 digest/checksum)
- XSHA512 (requests SHA512 digest/checksum)

and we also support the more modern HASH command

https://tools.ietf.org/html/draft-bryan-ftpext-hash-02

* move hash method from handle_misc to handle_files

* properly order hash command inside commandsMap

* add an optional hasher extension

* fix lint issues
  • Loading branch information
drakkan authored Dec 16, 2020
1 parent 6f53c30 commit c16c83a
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 105 deletions.
79 changes: 52 additions & 27 deletions client_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
7 changes: 7 additions & 0 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
151 changes: 151 additions & 0 deletions handle_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 <file> <start> <end>
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
}
Loading

0 comments on commit c16c83a

Please sign in to comment.