mirror of
https://github.com/rclone/rclone.git
synced 2024-12-19 13:41:21 +01:00
6b158f33a3
See #6351
319 lines
8.3 KiB
Go
319 lines
8.3 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 legacy rclone command detection.
|
|
// Before rclone v1.49.0 the sftp backend used "echo 'abc' | md5sum" when
|
|
// detecting hash support, but was then changed to instead just execute
|
|
// md5sum/sha1sum (without arguments), which is handled above. The following
|
|
// code is therefore only necessary to support rclone versions older than
|
|
// v1.49.0 using a sftp remote connected to a rclone serve sftp instance
|
|
// running a newer version of rclone (e.g. latest).
|
|
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
|
|
}
|