rclone/cmd/serve/sftp/connection.go
Nick Craig-Wood 98fd00a655 serve sftp: fix checksum detection - Fixes #6351
Before this change, rclone serve sftp operating with a new rclone
after the md5sum/sha1sum detection was reworked to just run a plain
`md5sum`/`sha1sum` command in

3ea82032e7 sftp: support md5/sha1 with rsync.net #3254

Failed to signal to the remote that md5sum/sha1sum wasn't supported as
in

71e172a139 serve/sftp: support empty "md5sum" and "sha1sum" commands

We unconditionally return good hashes even if the remote being served
doesn't support the hash type in question.

This fix checks the hash type is supported and returns an error

    MD5 hash not supported

When the backend is first contacted this will cause the sftp backend
to detect that the hash type isn't available.

Unfortunately this may have cached the wrong state so editing or
remaking the config may be necessary to fix it.
2022-08-05 17:16:23 +01:00

312 lines
7.9 KiB
Go

//go:build !plan9
// +build !plan9
package sftp
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"regexp"
"strings"
"github.com/pkg/sftp"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/lib/terminal"
"github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfsflags"
"golang.org/x/crypto/ssh"
)
func describeConn(c interface {
RemoteAddr() net.Addr
LocalAddr() net.Addr
}) string {
return fmt.Sprintf("serve sftp %s->%s", c.RemoteAddr(), c.LocalAddr())
}
// Return the exit status of the command
type exitStatus struct {
RC uint32
}
// The incoming exec command
type execCommand struct {
Command string
}
var shellUnEscapeRegex = regexp.MustCompile(`\\(.)`)
// Unescape a string that was escaped by rclone
func shellUnEscape(str string) string {
str = strings.ReplaceAll(str, "'\n'", "\n")
str = shellUnEscapeRegex.ReplaceAllString(str, `$1`)
return str
}
// Info about the current connection
type conn struct {
vfs *vfs.VFS
handlers sftp.Handlers
what string
}
// execCommand implements an extremely limited number of commands to
// interoperate with the rclone sftp backend
func (c *conn) execCommand(ctx context.Context, out io.Writer, command string) (err error) {
binary, args := command, ""
space := strings.Index(command, " ")
if space >= 0 {
binary = command[:space]
args = strings.TrimLeft(command[space+1:], " ")
}
args = shellUnEscape(args)
fs.Debugf(c.what, "exec command: binary = %q, args = %q", binary, args)
switch binary {
case "df":
about := c.vfs.Fs().Features().About
if about == nil {
return errors.New("df not supported")
}
usage, err := about(ctx)
if err != nil {
return fmt.Errorf("about failed: %w", err)
}
total, used, free := int64(-1), int64(-1), int64(-1)
if usage.Total != nil {
total = *usage.Total / 1024
}
if usage.Used != nil {
used = *usage.Used / 1024
}
if usage.Free != nil {
free = *usage.Free / 1024
}
perc := int64(0)
if total > 0 && used >= 0 {
perc = (100 * used) / total
}
_, err = fmt.Fprintf(out, ` Filesystem 1K-blocks Used Available Use%% Mounted on
/dev/root %d %d %d %d%% /
`, total, used, free, perc)
if err != nil {
return fmt.Errorf("send output failed: %w", err)
}
case "md5sum", "sha1sum":
ht := hash.MD5
if binary == "sha1sum" {
ht = hash.SHA1
}
if !c.vfs.Fs().Hashes().Contains(ht) {
return fmt.Errorf("%v hash not supported", ht)
}
var hashSum string
if args == "" {
// empty hash for no input
if ht == hash.MD5 {
hashSum = "d41d8cd98f00b204e9800998ecf8427e"
} else {
hashSum = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
args = "-"
} else {
node, err := c.vfs.Stat(args)
if err != nil {
return fmt.Errorf("hash failed finding file %q: %w", args, err)
}
if node.IsDir() {
return errors.New("can't hash directory")
}
o, ok := node.DirEntry().(fs.ObjectInfo)
if !ok {
return errors.New("unexpected non file")
}
hashSum, err = o.Hash(ctx, ht)
if err != nil {
return fmt.Errorf("hash failed: %w", err)
}
}
_, err = fmt.Fprintf(out, "%s %s\n", hashSum, args)
if err != nil {
return fmt.Errorf("send output failed: %w", err)
}
case "echo":
// special cases for rclone command detection
switch args {
case "'abc' | md5sum":
if c.vfs.Fs().Hashes().Contains(hash.MD5) {
_, err = fmt.Fprintf(out, "0bee89b07a248e27c83fc3d5951213c1 -\n")
if err != nil {
return fmt.Errorf("send output failed: %w", err)
}
} else {
return errors.New("md5 hash not supported")
}
case "'abc' | sha1sum":
if c.vfs.Fs().Hashes().Contains(hash.SHA1) {
_, err = fmt.Fprintf(out, "03cfd743661f07975fa2f1220c5194cbaff48451 -\n")
if err != nil {
return fmt.Errorf("send output failed: %w", err)
}
} else {
return errors.New("sha1 hash not supported")
}
default:
_, err = fmt.Fprintf(out, "%s\n", args)
if err != nil {
return fmt.Errorf("send output failed: %w", err)
}
}
default:
return fmt.Errorf("%q not implemented", command)
}
return nil
}
// handle a new incoming channel request
func (c *conn) handleChannel(newChannel ssh.NewChannel) {
fs.Debugf(c.what, "Incoming channel: %s\n", newChannel.ChannelType())
if newChannel.ChannelType() != "session" {
err := newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
fs.Debugf(c.what, "Unknown channel type: %s\n", newChannel.ChannelType())
if err != nil {
fs.Errorf(c.what, "Failed to reject unknown channel: %v", err)
}
return
}
channel, requests, err := newChannel.Accept()
if err != nil {
fs.Errorf(c.what, "could not accept channel: %v", err)
return
}
defer func() {
err := channel.Close()
if err != nil && err != io.EOF {
fs.Debugf(c.what, "Failed to close channel: %v", err)
}
}()
fs.Debugf(c.what, "Channel accepted\n")
isSFTP := make(chan bool, 1)
var command execCommand
// Handle out-of-band requests
go func(in <-chan *ssh.Request) {
for req := range in {
fs.Debugf(c.what, "Request: %v\n", req.Type)
ok := false
var subSystemIsSFTP bool
var reply []byte
switch req.Type {
case "subsystem":
fs.Debugf(c.what, "Subsystem: %s\n", req.Payload[4:])
if string(req.Payload[4:]) == "sftp" {
ok = true
subSystemIsSFTP = true
}
case "exec":
err := ssh.Unmarshal(req.Payload, &command)
if err != nil {
fs.Errorf(c.what, "ignoring bad exec command: %v", err)
} else {
ok = true
subSystemIsSFTP = false
}
}
fs.Debugf(c.what, " - accepted: %v\n", ok)
err = req.Reply(ok, reply)
if err != nil {
fs.Errorf(c.what, "Failed to Reply to request: %v", err)
return
}
if ok {
// Wake up main routine after we have responded
isSFTP <- subSystemIsSFTP
}
}
}(requests)
// Wait for either subsystem "sftp" or "exec" request
if <-isSFTP {
if err := serveChannel(channel, c.handlers, c.what); err != nil {
fs.Errorf(c.what, "Failed to serve SFTP: %v", err)
}
} else {
var rc = uint32(0)
err := c.execCommand(context.TODO(), channel, command.Command)
if err != nil {
rc = 1
_, errPrint := fmt.Fprintf(channel.Stderr(), "%v\n", err)
if errPrint != nil {
fs.Errorf(c.what, "Failed to write to stderr: %v", errPrint)
}
fs.Debugf(c.what, "command %q failed with error: %v", command.Command, err)
}
_, err = channel.SendRequest("exit-status", false, ssh.Marshal(exitStatus{RC: rc}))
if err != nil {
fs.Errorf(c.what, "Failed to send exit status: %v", err)
}
}
}
// Service the incoming Channel channel in go routine
func (c *conn) handleChannels(chans <-chan ssh.NewChannel) {
for newChannel := range chans {
go c.handleChannel(newChannel)
}
}
func serveChannel(rwc io.ReadWriteCloser, h sftp.Handlers, what string) error {
fs.Debugf(what, "Starting SFTP server")
server := sftp.NewRequestServer(rwc, h)
defer func() {
err := server.Close()
if err != nil && err != io.EOF {
fs.Debugf(what, "Failed to close server: %v", err)
}
}()
err := server.Serve()
if err != nil && err != io.EOF {
return fmt.Errorf("completed with error: %w", err)
}
fs.Debugf(what, "exited session")
return nil
}
func serveStdio(f fs.Fs) error {
if terminal.IsTerminal(int(os.Stdout.Fd())) {
return errors.New("refusing to run SFTP server directly on a terminal. Please let sshd start rclone, by connecting with sftp or sshfs")
}
sshChannel := &stdioChannel{
stdin: os.Stdin,
stdout: os.Stdout,
}
handlers := newVFSHandler(vfs.New(f, &vfsflags.Opt))
return serveChannel(sshChannel, handlers, "stdio")
}
type stdioChannel struct {
stdin *os.File
stdout *os.File
}
func (c *stdioChannel) Read(data []byte) (int, error) {
return c.stdin.Read(data)
}
func (c *stdioChannel) Write(data []byte) (int, error) {
return c.stdout.Write(data)
}
func (c *stdioChannel) Close() error {
err1 := c.stdin.Close()
err2 := c.stdout.Close()
if err1 != nil {
return err1
}
return err2
}