serve sftp: add serve rc interface

This commit is contained in:
Nick Craig-Wood 2025-04-02 18:05:23 +01:00
parent 703788b40e
commit 5702b7578c
3 changed files with 82 additions and 43 deletions

View File

@ -16,6 +16,7 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
@ -40,23 +41,27 @@ type server struct {
ctx context.Context // for global config ctx context.Context // for global config
config *ssh.ServerConfig config *ssh.ServerConfig
listener net.Listener listener net.Listener
waitChan chan struct{} // for waiting on the listener to close stopped chan struct{} // for waiting on the listener to stop
proxy *proxy.Proxy proxy *proxy.Proxy
} }
func newServer(ctx context.Context, f fs.Fs, opt *Options) *server { func newServer(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Options, proxyOpt *proxy.Options) (*server, error) {
s := &server{ s := &server{
f: f, f: f,
ctx: ctx, ctx: ctx,
opt: *opt, opt: *opt,
waitChan: make(chan struct{}), stopped: make(chan struct{}),
} }
if proxy.Opt.AuthProxy != "" { if proxy.Opt.AuthProxy != "" {
s.proxy = proxy.New(ctx, &proxy.Opt, &vfscommon.Opt) s.proxy = proxy.New(ctx, proxyOpt, vfsOpt)
} else { } else {
s.vfs = vfs.New(f, &vfscommon.Opt) s.vfs = vfs.New(f, vfsOpt)
} }
return s err := s.configure()
if err != nil {
return nil, fmt.Errorf("sftp configuration failed: %w", err)
}
return s, nil
} }
// getVFS gets the vfs from s or the proxy // getVFS gets the vfs from s or the proxy
@ -128,8 +133,10 @@ func (s *server) acceptConnections() {
} }
} }
// configure the server
//
// Based on example server code from golang.org/x/crypto/ssh and server_standalone // Based on example server code from golang.org/x/crypto/ssh and server_standalone
func (s *server) serve() (err error) { func (s *server) configure() (err error) {
var authorizedKeysMap map[string]struct{} var authorizedKeysMap map[string]struct{}
// ensure the user isn't trying to use conflicting flags // ensure the user isn't trying to use conflicting flags
@ -292,42 +299,35 @@ func (s *server) serve() (err error) {
} }
} }
s.listener = listener s.listener = listener
return nil
}
// Serve SFTP until the server is Shutdown
func (s *server) Serve() (err error) {
fs.Logf(nil, "SFTP server listening on %v\n", s.listener.Addr()) fs.Logf(nil, "SFTP server listening on %v\n", s.listener.Addr())
s.acceptConnections()
go s.acceptConnections() close(s.stopped)
return nil return nil
} }
// Addr returns the address the server is listening on // Addr returns the address the server is listening on
func (s *server) Addr() string { func (s *server) Addr() net.Addr {
return s.listener.Addr().String() return s.listener.Addr()
}
// Serve runs the sftp server in the background.
//
// Use s.Close() and s.Wait() to shutdown server
func (s *server) Serve() error {
err := s.serve()
if err != nil {
return err
}
return nil
} }
// Wait blocks while the listener is open. // Wait blocks while the listener is open.
func (s *server) Wait() { func (s *server) Wait() {
<-s.waitChan <-s.stopped
} }
// Close shuts the running server down // Shutdown shuts the running server down
func (s *server) Close() { func (s *server) Shutdown() error {
err := s.listener.Close() err := s.listener.Close()
if err != nil { if errors.Is(err, io.ErrUnexpectedEOF) {
fs.Errorf(nil, "Error on closing SFTP server: %v", err) err = nil
return
} }
close(s.waitChan) s.Wait()
return err
} }
func loadPrivateKey(keyPath string) (ssh.Signer, error) { func loadPrivateKey(keyPath string) (ssh.Signer, error) {

View File

@ -5,15 +5,19 @@ package sftp
import ( import (
"context" "context"
"fmt"
"github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/cmd/serve" "github.com/rclone/rclone/cmd/serve"
"github.com/rclone/rclone/cmd/serve/proxy" "github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags" "github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/lib/systemd" "github.com/rclone/rclone/lib/systemd"
"github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/rclone/rclone/vfs/vfsflags" "github.com/rclone/rclone/vfs/vfsflags"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -78,6 +82,28 @@ func init() {
proxyflags.AddFlags(Command.Flags()) proxyflags.AddFlags(Command.Flags())
AddFlags(Command.Flags(), &Opt) AddFlags(Command.Flags(), &Opt)
serve.Command.AddCommand(Command) serve.Command.AddCommand(Command)
serve.AddRc("sftp", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) {
// Read VFS Opts
var vfsOpt = vfscommon.Opt // set default opts
err := configstruct.SetAny(in, &vfsOpt)
if err != nil {
return nil, err
}
// Read Proxy Opts
var proxyOpt = proxy.Opt // set default opts
err = configstruct.SetAny(in, &proxyOpt)
if err != nil {
return nil, err
}
// Read opts
var opt = Opt // set default opts
err = configstruct.SetAny(in, &opt)
if err != nil {
return nil, err
}
// Create server
return newServer(ctx, f, &opt, &vfsOpt, &proxyOpt)
})
} }
// Command definition for cobra // Command definition for cobra
@ -164,14 +190,12 @@ provided by OpenSSH in this case.
if Opt.Stdio { if Opt.Stdio {
return serveStdio(f) return serveStdio(f)
} }
s := newServer(context.Background(), f, &Opt) s, err := newServer(context.Background(), f, &Opt, &vfscommon.Opt, &proxy.Opt)
err := s.Serve()
if err != nil { if err != nil {
return err fs.Fatal(nil, fmt.Sprint(err))
} }
defer systemd.Notify()() defer systemd.Notify()()
s.Wait() return s.Serve()
return nil
}) })
}, },
} }

View File

@ -14,10 +14,14 @@ import (
"github.com/pkg/sftp" "github.com/pkg/sftp"
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/cmd/serve/proxy"
"github.com/rclone/rclone/cmd/serve/servetest" "github.com/rclone/rclone/cmd/serve/servetest"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -45,11 +49,14 @@ func TestSftp(t *testing.T) {
opt.User = testUser opt.User = testUser
opt.Pass = testPass opt.Pass = testPass
w := newServer(context.Background(), f, &opt) w, err := newServer(context.Background(), f, &opt, &vfscommon.Opt, &proxy.Opt)
require.NoError(t, w.serve()) require.NoError(t, err)
go func() {
require.NoError(t, w.Serve())
}()
// Read the host and port we started on // Read the host and port we started on
addr := w.Addr() addr := w.Addr().String()
colon := strings.LastIndex(addr, ":") colon := strings.LastIndex(addr, ":")
// Config for the backend we'll use to connect to the server // Config for the backend we'll use to connect to the server
@ -63,10 +70,18 @@ func TestSftp(t *testing.T) {
// return a stop function // return a stop function
return config, func() { return config, func() {
w.Close() assert.NoError(t, w.Shutdown())
w.Wait()
} }
} }
servetest.Run(t, "sftp", start) servetest.Run(t, "sftp", start)
} }
func TestRc(t *testing.T) {
servetest.TestRc(t, rc.Params{
"type": "sftp",
"user": "test",
"pass": obscure.MustObscure("test"),
"vfs_cache_mode": "off",
})
}