github.com/divyam234/rclone@v1.64.1/cmd/serve/sftp/connection.go (about)

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