98fd00a655
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 in3ea82032e7
sftp: support md5/sha1 with rsync.net #3254 Failed to signal to the remote that md5sum/sha1sum wasn't supported as in71e172a139
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.
312 lines
7.9 KiB
Go
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
|
|
}
|