mirror of
https://github.com/rclone/rclone.git
synced 2024-11-08 17:44:57 +01:00
255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
// +build !plan9
|
|
|
|
package sftp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/pkg/sftp"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
"github.com/rclone/rclone/vfs"
|
|
"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.Replace(str, "'\n'", "\n", -1)
|
|
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 extrememly 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 errors.Wrap(err, "About failed")
|
|
}
|
|
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 errors.Wrap(err, "send output failed")
|
|
}
|
|
case "md5sum", "sha1sum":
|
|
ht := hash.MD5
|
|
if binary == "sha1sum" {
|
|
ht = hash.SHA1
|
|
}
|
|
node, err := c.vfs.Stat(args)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "hash failed finding file %q", args)
|
|
}
|
|
if node.IsDir() {
|
|
return errors.New("can't hash directory")
|
|
}
|
|
o, ok := node.DirEntry().(fs.ObjectInfo)
|
|
if !ok {
|
|
return errors.New("unexpected non file")
|
|
}
|
|
hash, err := o.Hash(ctx, ht)
|
|
if err != nil {
|
|
return errors.Wrap(err, "hash failed")
|
|
}
|
|
_, err = fmt.Fprintf(out, "%s %s\n", hash, args)
|
|
if err != nil {
|
|
return errors.Wrap(err, "send output failed")
|
|
}
|
|
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 errors.Wrap(err, "send output failed")
|
|
}
|
|
} 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 errors.Wrap(err, "send output failed")
|
|
}
|
|
} else {
|
|
return errors.New("sha1 hash not supported")
|
|
}
|
|
default:
|
|
_, err = fmt.Fprintf(out, "%s\n", args)
|
|
if err != nil {
|
|
return errors.Wrap(err, "send output failed")
|
|
}
|
|
}
|
|
default:
|
|
return errors.Errorf("%q not implemented\n", 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 {
|
|
fs.Debugf(c.what, "Starting SFTP server")
|
|
server := sftp.NewRequestServer(channel, c.handlers)
|
|
defer func() {
|
|
err := server.Close()
|
|
if err != nil && err != io.EOF {
|
|
fs.Debugf(c.what, "Failed to close server: %v", err)
|
|
}
|
|
}()
|
|
err = server.Serve()
|
|
if err == io.EOF || err == nil {
|
|
fs.Debugf(c.what, "exited session")
|
|
} else {
|
|
fs.Errorf(c.what, "completed with error: %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)
|
|
}
|
|
}
|