github.com/artpar/rclone@v1.67.3/cmd/serve/sftp/connection.go (about)

     1  //go:build !plan9
     2  
     3  package sftp
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"os"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/artpar/rclone/fs"
    16  	"github.com/artpar/rclone/fs/hash"
    17  	"github.com/artpar/rclone/lib/terminal"
    18  	"github.com/artpar/rclone/vfs"
    19  	"github.com/artpar/rclone/vfs/vfsflags"
    20  	"github.com/pkg/sftp"
    21  	"golang.org/x/crypto/ssh"
    22  )
    23  
    24  func describeConn(c interface {
    25  	RemoteAddr() net.Addr
    26  	LocalAddr() net.Addr
    27  }) string {
    28  	return fmt.Sprintf("serve sftp %s->%s", c.RemoteAddr(), c.LocalAddr())
    29  }
    30  
    31  // Return the exit status of the command
    32  type exitStatus struct {
    33  	RC uint32
    34  }
    35  
    36  // The incoming exec command
    37  type execCommand struct {
    38  	Command string
    39  }
    40  
    41  var shellUnEscapeRegex = regexp.MustCompile(`\\(.)`)
    42  
    43  // Unescape a string that was escaped by rclone
    44  func shellUnEscape(str string) string {
    45  	str = strings.ReplaceAll(str, "'\n'", "\n")
    46  	str = shellUnEscapeRegex.ReplaceAllString(str, `$1`)
    47  	return str
    48  }
    49  
    50  // Info about the current connection
    51  type conn struct {
    52  	vfs      *vfs.VFS
    53  	handlers sftp.Handlers
    54  	what     string
    55  }
    56  
    57  // execCommand implements an extremely limited number of commands to
    58  // interoperate with the rclone sftp backend
    59  func (c *conn) execCommand(ctx context.Context, out io.Writer, command string) (err error) {
    60  	binary, args := command, ""
    61  	space := strings.Index(command, " ")
    62  	if space >= 0 {
    63  		binary = command[:space]
    64  		args = strings.TrimLeft(command[space+1:], " ")
    65  	}
    66  	args = shellUnEscape(args)
    67  	fs.Debugf(c.what, "exec command: binary = %q, args = %q", binary, args)
    68  	switch binary {
    69  	case "df":
    70  		about := c.vfs.Fs().Features().About
    71  		if about == nil {
    72  			return errors.New("df not supported")
    73  		}
    74  		usage, err := about(ctx)
    75  		if err != nil {
    76  			return fmt.Errorf("about failed: %w", err)
    77  		}
    78  		total, used, free := int64(-1), int64(-1), int64(-1)
    79  		if usage.Total != nil {
    80  			total = *usage.Total / 1024
    81  		}
    82  		if usage.Used != nil {
    83  			used = *usage.Used / 1024
    84  		}
    85  		if usage.Free != nil {
    86  			free = *usage.Free / 1024
    87  		}
    88  		perc := int64(0)
    89  		if total > 0 && used >= 0 {
    90  			perc = (100 * used) / total
    91  		}
    92  		_, err = fmt.Fprintf(out, `		Filesystem                   1K-blocks      Used Available Use%% Mounted on
    93  /dev/root %d %d  %d  %d%% /
    94  `, total, used, free, perc)
    95  		if err != nil {
    96  			return fmt.Errorf("send output failed: %w", err)
    97  		}
    98  	case "md5sum", "sha1sum":
    99  		ht := hash.MD5
   100  		if binary == "sha1sum" {
   101  			ht = hash.SHA1
   102  		}
   103  		if !c.vfs.Fs().Hashes().Contains(ht) {
   104  			return fmt.Errorf("%v hash not supported", ht)
   105  		}
   106  		var hashSum string
   107  		if args == "" {
   108  			// empty hash for no input
   109  			if ht == hash.MD5 {
   110  				hashSum = "d41d8cd98f00b204e9800998ecf8427e"
   111  			} else {
   112  				hashSum = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
   113  			}
   114  			args = "-"
   115  		} else {
   116  			node, err := c.vfs.Stat(args)
   117  			if err != nil {
   118  				return fmt.Errorf("hash failed finding file %q: %w", args, err)
   119  			}
   120  			if node.IsDir() {
   121  				return errors.New("can't hash directory")
   122  			}
   123  			o, ok := node.DirEntry().(fs.ObjectInfo)
   124  			if !ok {
   125  				fs.Debugf(args, "File uploading - reading hash from VFS cache")
   126  				in, err := node.Open(os.O_RDONLY)
   127  				if err != nil {
   128  					return fmt.Errorf("hash vfs open failed: %w", err)
   129  				}
   130  				defer func() {
   131  					_ = in.Close()
   132  				}()
   133  				h, err := hash.NewMultiHasherTypes(hash.NewHashSet(ht))
   134  				if err != nil {
   135  					return fmt.Errorf("hash vfs create multi-hasher failed: %w", err)
   136  				}
   137  				_, err = io.Copy(h, in)
   138  				if err != nil {
   139  					return fmt.Errorf("hash vfs copy failed: %w", err)
   140  				}
   141  				hashSum = h.Sums()[ht]
   142  			} else {
   143  				hashSum, err = o.Hash(ctx, ht)
   144  				if err != nil {
   145  					return fmt.Errorf("hash failed: %w", err)
   146  				}
   147  			}
   148  		}
   149  		_, err = fmt.Fprintf(out, "%s  %s\n", hashSum, args)
   150  		if err != nil {
   151  			return fmt.Errorf("send output failed: %w", err)
   152  		}
   153  	case "echo":
   154  		// Special cases for legacy rclone command detection.
   155  		// Before rclone v1.49.0 the sftp backend used "echo 'abc' | md5sum" when
   156  		// detecting hash support, but was then changed to instead just execute
   157  		// md5sum/sha1sum (without arguments), which is handled above. The following
   158  		// code is therefore only necessary to support rclone versions older than
   159  		// v1.49.0 using a sftp remote connected to a rclone serve sftp instance
   160  		// running a newer version of rclone (e.g. latest).
   161  		switch args {
   162  		case "'abc' | md5sum":
   163  			if c.vfs.Fs().Hashes().Contains(hash.MD5) {
   164  				_, err = fmt.Fprintf(out, "0bee89b07a248e27c83fc3d5951213c1  -\n")
   165  				if err != nil {
   166  					return fmt.Errorf("send output failed: %w", err)
   167  				}
   168  			} else {
   169  				return errors.New("md5 hash not supported")
   170  			}
   171  		case "'abc' | sha1sum":
   172  			if c.vfs.Fs().Hashes().Contains(hash.SHA1) {
   173  				_, err = fmt.Fprintf(out, "03cfd743661f07975fa2f1220c5194cbaff48451  -\n")
   174  				if err != nil {
   175  					return fmt.Errorf("send output failed: %w", err)
   176  				}
   177  			} else {
   178  				return errors.New("sha1 hash not supported")
   179  			}
   180  		default:
   181  			_, err = fmt.Fprintf(out, "%s\n", args)
   182  			if err != nil {
   183  				return fmt.Errorf("send output failed: %w", err)
   184  			}
   185  		}
   186  	default:
   187  		return fmt.Errorf("%q not implemented", command)
   188  	}
   189  	return nil
   190  }
   191  
   192  // handle a new incoming channel request
   193  func (c *conn) handleChannel(newChannel ssh.NewChannel) {
   194  	fs.Debugf(c.what, "Incoming channel: %s\n", newChannel.ChannelType())
   195  	if newChannel.ChannelType() != "session" {
   196  		err := newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
   197  		fs.Debugf(c.what, "Unknown channel type: %s\n", newChannel.ChannelType())
   198  		if err != nil {
   199  			fs.Errorf(c.what, "Failed to reject unknown channel: %v", err)
   200  		}
   201  		return
   202  	}
   203  	channel, requests, err := newChannel.Accept()
   204  	if err != nil {
   205  		fs.Errorf(c.what, "could not accept channel: %v", err)
   206  		return
   207  	}
   208  	defer func() {
   209  		err := channel.Close()
   210  		if err != nil && err != io.EOF {
   211  			fs.Debugf(c.what, "Failed to close channel: %v", err)
   212  		}
   213  	}()
   214  	fs.Debugf(c.what, "Channel accepted\n")
   215  
   216  	isSFTP := make(chan bool, 1)
   217  	var command execCommand
   218  
   219  	// Handle out-of-band requests
   220  	go func(in <-chan *ssh.Request) {
   221  		for req := range in {
   222  			fs.Debugf(c.what, "Request: %v\n", req.Type)
   223  			ok := false
   224  			var subSystemIsSFTP bool
   225  			var reply []byte
   226  			switch req.Type {
   227  			case "subsystem":
   228  				fs.Debugf(c.what, "Subsystem: %s\n", req.Payload[4:])
   229  				if string(req.Payload[4:]) == "sftp" {
   230  					ok = true
   231  					subSystemIsSFTP = true
   232  				}
   233  			case "exec":
   234  				err := ssh.Unmarshal(req.Payload, &command)
   235  				if err != nil {
   236  					fs.Errorf(c.what, "ignoring bad exec command: %v", err)
   237  				} else {
   238  					ok = true
   239  					subSystemIsSFTP = false
   240  				}
   241  			}
   242  			fs.Debugf(c.what, " - accepted: %v\n", ok)
   243  			err = req.Reply(ok, reply)
   244  			if err != nil {
   245  				fs.Errorf(c.what, "Failed to Reply to request: %v", err)
   246  				return
   247  			}
   248  			if ok {
   249  				// Wake up main routine after we have responded
   250  				isSFTP <- subSystemIsSFTP
   251  			}
   252  		}
   253  	}(requests)
   254  
   255  	// Wait for either subsystem "sftp" or "exec" request
   256  	if <-isSFTP {
   257  		if err := serveChannel(channel, c.handlers, c.what); err != nil {
   258  			fs.Errorf(c.what, "Failed to serve SFTP: %v", err)
   259  		}
   260  	} else {
   261  		var rc = uint32(0)
   262  		err := c.execCommand(context.TODO(), channel, command.Command)
   263  		if err != nil {
   264  			rc = 1
   265  			_, errPrint := fmt.Fprintf(channel.Stderr(), "%v\n", err)
   266  			if errPrint != nil {
   267  				fs.Errorf(c.what, "Failed to write to stderr: %v", errPrint)
   268  			}
   269  			fs.Debugf(c.what, "command %q failed with error: %v", command.Command, err)
   270  		}
   271  		_, err = channel.SendRequest("exit-status", false, ssh.Marshal(exitStatus{RC: rc}))
   272  		if err != nil {
   273  			fs.Errorf(c.what, "Failed to send exit status: %v", err)
   274  		}
   275  	}
   276  }
   277  
   278  // Service the incoming Channel channel in go routine
   279  func (c *conn) handleChannels(chans <-chan ssh.NewChannel) {
   280  	for newChannel := range chans {
   281  		go c.handleChannel(newChannel)
   282  	}
   283  }
   284  
   285  func serveChannel(rwc io.ReadWriteCloser, h sftp.Handlers, what string) error {
   286  	fs.Debugf(what, "Starting SFTP server")
   287  	server := sftp.NewRequestServer(rwc, h)
   288  	defer func() {
   289  		err := server.Close()
   290  		if err != nil && err != io.EOF {
   291  			fs.Debugf(what, "Failed to close server: %v", err)
   292  		}
   293  	}()
   294  	err := server.Serve()
   295  	if err != nil && err != io.EOF {
   296  		return fmt.Errorf("completed with error: %w", err)
   297  	}
   298  	fs.Debugf(what, "exited session")
   299  	return nil
   300  }
   301  
   302  func serveStdio(f fs.Fs) error {
   303  	if terminal.IsTerminal(int(os.Stdout.Fd())) {
   304  		return errors.New("refusing to run SFTP server directly on a terminal. Please let sshd start rclone, by connecting with sftp or sshfs")
   305  	}
   306  	sshChannel := &stdioChannel{
   307  		stdin:  os.Stdin,
   308  		stdout: os.Stdout,
   309  	}
   310  	handlers := newVFSHandler(vfs.New(f, &vfsflags.Opt))
   311  	return serveChannel(sshChannel, handlers, "stdio")
   312  }
   313  
   314  type stdioChannel struct {
   315  	stdin  *os.File
   316  	stdout *os.File
   317  }
   318  
   319  func (c *stdioChannel) Read(data []byte) (int, error) {
   320  	return c.stdin.Read(data)
   321  }
   322  
   323  func (c *stdioChannel) Write(data []byte) (int, error) {
   324  	return c.stdout.Write(data)
   325  }
   326  
   327  func (c *stdioChannel) Close() error {
   328  	err1 := c.stdin.Close()
   329  	err2 := c.stdout.Close()
   330  	if err1 != nil {
   331  		return err1
   332  	}
   333  	return err2
   334  }