sftpd: add support for some SSH commands

md5sum, sha1sum are used by rclone.
cd, pwd improve the support for RemoteFiles mobile app.

These commands are all implemented inside SFTPGo so they work even
if the matching system commands are not available, for example on Windows
This commit is contained in:
Nicola Murino
2019-11-18 23:30:37 +01:00
parent ca6cb34d98
commit 9c4dbbc3f8
14 changed files with 564 additions and 131 deletions

173
sftpd/ssh_cmd.go Normal file
View File

@@ -0,0 +1,173 @@
package sftpd
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/utils"
"golang.org/x/crypto/ssh"
)
type sshCommand struct {
command string
args []string
connection Connection
}
func processSSHCommand(payload []byte, connection *Connection, channel ssh.Channel, enabledSSHCommands []string) bool {
var msg sshSubsystemExecMsg
if err := ssh.Unmarshal(payload, &msg); err == nil {
name, args, err := parseCommandPayload(msg.Command)
connection.Log(logger.LevelDebug, logSenderSSH, "new ssh command: %#v args: %v user: %v, error: %v",
name, args, connection.User.Username, err)
if err == nil && utils.IsStringInSlice(name, enabledSSHCommands) {
connection.command = fmt.Sprintf("%v %v", name, strings.Join(args, " "))
if name == "scp" && len(args) >= 2 {
connection.protocol = protocolSCP
connection.channel = channel
scpCommand := scpCommand{
sshCommand: sshCommand{
command: name,
connection: *connection,
args: args},
}
go scpCommand.handle()
return true
}
if name != "scp" {
connection.protocol = protocolSSH
connection.channel = channel
sshCommand := sshCommand{
command: name,
connection: *connection,
args: args,
}
go sshCommand.handle()
return true
}
} else {
connection.Log(logger.LevelInfo, logSenderSSH, "ssh command not enabled/supported: %#v", name)
}
}
return false
}
func (c *sshCommand) handle() error {
addConnection(c.connection)
defer removeConnection(c.connection)
updateConnectionActivity(c.connection.ID)
if utils.IsStringInSlice(c.command, sshHashCommands) {
var h hash.Hash
if c.command == "md5sum" {
h = md5.New()
} else if c.command == "sha1sum" {
h = sha1.New()
} else if c.command == "sha256sum" {
h = sha256.New()
} else if c.command == "sha384sum" {
h = sha512.New384()
} else {
h = sha512.New()
}
var response string
if len(c.args) == 0 {
// without args we need to read the string to hash from stdin
buf := make([]byte, 4096)
n, err := c.connection.channel.Read(buf)
if err != nil && err != io.EOF {
return c.sendErrorResponse(err)
}
h.Write(buf[:n])
response = fmt.Sprintf("%x -\n", h.Sum(nil))
} else {
sshPath := c.getDestPath()
path, err := c.connection.buildPath(sshPath)
if err != nil {
return c.sendErrorResponse(err)
}
hash, err := computeHashForFile(h, path)
if err != nil {
return c.sendErrorResponse(err)
}
response = fmt.Sprintf("%v %v\n", hash, sshPath)
}
c.connection.channel.Write([]byte(response))
c.sendExitStatus(nil)
} else if c.command == "cd" {
c.sendExitStatus(nil)
} else if c.command == "pwd" {
// hard coded response to "/"
c.connection.channel.Write([]byte("/\n"))
c.sendExitStatus(nil)
}
return nil
}
// for the supported command, the path, if any, is the last argument
func (c *sshCommand) getDestPath() string {
if len(c.args) == 0 {
return ""
}
destPath := filepath.ToSlash(c.args[len(c.args)-1])
if !path.IsAbs(destPath) {
destPath = "/" + destPath
}
result := path.Clean(destPath)
if strings.HasSuffix(destPath, "/") && !strings.HasSuffix(result, "/") {
result += "/"
}
return result
}
func (c *sshCommand) sendErrorResponse(err error) error {
errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), err)
c.connection.channel.Write([]byte(errorString))
c.sendExitStatus(err)
return err
}
func (c *sshCommand) sendExitStatus(err error) {
status := uint32(0)
if err != nil {
status = uint32(1)
}
exitStatus := sshSubsystemExitStatus{
Status: status,
}
c.connection.Log(logger.LevelDebug, logSenderSSH, "send exit status for command %#v with args: %v user: %v err: %v",
c.command, c.args, c.connection.User.Username, err)
c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus))
c.connection.channel.Close()
}
func computeHashForFile(hasher hash.Hash, path string) (string, error) {
hash := ""
f, err := os.Open(path)
if err != nil {
return hash, err
}
defer f.Close()
_, err = io.Copy(hasher, f)
if err == nil {
hash = fmt.Sprintf("%x", hasher.Sum(nil))
}
return hash, err
}
func parseCommandPayload(command string) (string, []string, error) {
parts := strings.Split(command, " ")
if len(parts) < 2 {
return parts[0], []string{}, nil
}
return parts[0], parts[1:], nil
}