e43b5ce5e5
This is possible now that we no longer support go1.12 and brings rclone into line with standard practices in the Go world. This also removes errors.New and errors.Errorf from lib/errors and prefers the stdlib errors package over lib/errors.
309 lines
7.8 KiB
Go
309 lines
7.8 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.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 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
|
|
}
|
|
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
|
|
}
|