You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							194 lines
						
					
					
						
							8.0 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							194 lines
						
					
					
						
							8.0 KiB
						
					
					
				
								package command
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util/version"
							 | 
						|
									"net"
							 | 
						|
									"os"
							 | 
						|
									"runtime"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb"
							 | 
						|
									filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/security"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/sftpd"
							 | 
						|
									stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								var (
							 | 
						|
									sftpOptionsStandalone SftpOptions
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// SftpOptions holds configuration options for the SFTP server.
							 | 
						|
								type SftpOptions struct {
							 | 
						|
									filer               *string
							 | 
						|
									bindIp              *string
							 | 
						|
									port                *int
							 | 
						|
									sshPrivateKey       *string
							 | 
						|
									hostKeysFolder      *string
							 | 
						|
									authMethods         *string
							 | 
						|
									maxAuthTries        *int
							 | 
						|
									bannerMessage       *string
							 | 
						|
									loginGraceTime      *time.Duration
							 | 
						|
									clientAliveInterval *time.Duration
							 | 
						|
									clientAliveCountMax *int
							 | 
						|
									userStoreFile       *string
							 | 
						|
									dataCenter          *string
							 | 
						|
									metricsHttpPort     *int
							 | 
						|
									metricsHttpIp       *string
							 | 
						|
									localSocket         *string
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// cmdSftp defines the SFTP command similar to the S3 command.
							 | 
						|
								var cmdSftp = &Command{
							 | 
						|
									UsageLine: "sftp [-port=2022] [-filer=<ip:port>] [-sshPrivateKey=</path/to/private_key>]",
							 | 
						|
									Short:     "start an SFTP server that is backed by a SeaweedFS filer",
							 | 
						|
									Long: `Start an SFTP server that leverages the SeaweedFS filer service to handle file operations.
							 | 
						|
								
							 | 
						|
								Instead of reading from or writing to a local filesystem, all file operations
							 | 
						|
								are routed through the filer (filer_pb) gRPC API. This allows you to centralize
							 | 
						|
								your file management in SeaweedFS.
							 | 
						|
									`,
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func init() {
							 | 
						|
									// Register the command to avoid cyclic dependencies.
							 | 
						|
									cmdSftp.Run = runSftp
							 | 
						|
								
							 | 
						|
									sftpOptionsStandalone.filer = cmdSftp.Flag.String("filer", "localhost:8888", "filer server address (ip:port)")
							 | 
						|
									sftpOptionsStandalone.bindIp = cmdSftp.Flag.String("ip.bind", "0.0.0.0", "ip address to bind SFTP server")
							 | 
						|
									sftpOptionsStandalone.port = cmdSftp.Flag.Int("port", 2022, "SFTP server listen port")
							 | 
						|
									sftpOptionsStandalone.sshPrivateKey = cmdSftp.Flag.String("sshPrivateKey", "", "path to the SSH private key file for host authentication")
							 | 
						|
									sftpOptionsStandalone.hostKeysFolder = cmdSftp.Flag.String("hostKeysFolder", "", "path to folder containing SSH private key files for host authentication")
							 | 
						|
									sftpOptionsStandalone.authMethods = cmdSftp.Flag.String("authMethods", "password,publickey", "comma-separated list of allowed auth methods: password, publickey, keyboard-interactive")
							 | 
						|
									sftpOptionsStandalone.maxAuthTries = cmdSftp.Flag.Int("maxAuthTries", 6, "maximum number of authentication attempts per connection")
							 | 
						|
									sftpOptionsStandalone.bannerMessage = cmdSftp.Flag.String("bannerMessage", "SeaweedFS SFTP Server - Unauthorized access is prohibited", "message displayed before authentication")
							 | 
						|
									sftpOptionsStandalone.loginGraceTime = cmdSftp.Flag.Duration("loginGraceTime", 2*time.Minute, "timeout for authentication")
							 | 
						|
									sftpOptionsStandalone.clientAliveInterval = cmdSftp.Flag.Duration("clientAliveInterval", 5*time.Second, "interval for sending keep-alive messages")
							 | 
						|
									sftpOptionsStandalone.clientAliveCountMax = cmdSftp.Flag.Int("clientAliveCountMax", 3, "maximum number of missed keep-alive messages before disconnecting")
							 | 
						|
									sftpOptionsStandalone.userStoreFile = cmdSftp.Flag.String("userStoreFile", "", "path to JSON file containing user credentials and permissions")
							 | 
						|
									sftpOptionsStandalone.dataCenter = cmdSftp.Flag.String("dataCenter", "", "prefer to read and write to volumes in this data center")
							 | 
						|
									sftpOptionsStandalone.metricsHttpPort = cmdSftp.Flag.Int("metricsPort", 0, "Prometheus metrics listen port")
							 | 
						|
									sftpOptionsStandalone.metricsHttpIp = cmdSftp.Flag.String("metricsIp", "", "metrics listen ip. If empty, default to same as -ip.bind option.")
							 | 
						|
									sftpOptionsStandalone.localSocket = cmdSftp.Flag.String("localSocket", "", "default to /tmp/seaweedfs-sftp-<port>.sock")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// runSftp is the command entry point.
							 | 
						|
								func runSftp(cmd *Command, args []string) bool {
							 | 
						|
									// Load security configuration as done in other SeaweedFS services.
							 | 
						|
									util.LoadSecurityConfiguration()
							 | 
						|
								
							 | 
						|
									// Configure metrics
							 | 
						|
									switch {
							 | 
						|
									case *sftpOptionsStandalone.metricsHttpIp != "":
							 | 
						|
										// nothing to do, use sftpOptionsStandalone.metricsHttpIp
							 | 
						|
									case *sftpOptionsStandalone.bindIp != "":
							 | 
						|
										*sftpOptionsStandalone.metricsHttpIp = *sftpOptionsStandalone.bindIp
							 | 
						|
									}
							 | 
						|
									go stats_collect.StartMetricsServer(*sftpOptionsStandalone.metricsHttpIp, *sftpOptionsStandalone.metricsHttpPort)
							 | 
						|
								
							 | 
						|
									return sftpOptionsStandalone.startSftpServer()
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (sftpOpt *SftpOptions) startSftpServer() bool {
							 | 
						|
									filerAddress := pb.ServerAddress(*sftpOpt.filer)
							 | 
						|
									grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client")
							 | 
						|
								
							 | 
						|
									// metrics read from the filer
							 | 
						|
									var metricsAddress string
							 | 
						|
									var metricsIntervalSec int
							 | 
						|
									var filerGroup string
							 | 
						|
								
							 | 
						|
									// Connect to the filer service and try to retrieve basic configuration.
							 | 
						|
									for {
							 | 
						|
										err := pb.WithGrpcFilerClient(false, 0, filerAddress, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
											resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{})
							 | 
						|
											if err != nil {
							 | 
						|
												return fmt.Errorf("get filer %s configuration: %v", filerAddress, err)
							 | 
						|
											}
							 | 
						|
											metricsAddress, metricsIntervalSec = resp.MetricsAddress, int(resp.MetricsIntervalSec)
							 | 
						|
											filerGroup = resp.FilerGroup
							 | 
						|
											glog.V(0).Infof("SFTP read filer configuration, using filer at: %s", filerAddress)
							 | 
						|
											return nil
							 | 
						|
										})
							 | 
						|
										if err != nil {
							 | 
						|
											glog.V(0).Infof("Waiting to connect to filer %s grpc address %s...", *sftpOpt.filer, filerAddress.ToGrpcAddress())
							 | 
						|
											time.Sleep(time.Second)
							 | 
						|
										} else {
							 | 
						|
											glog.V(0).Infof("Connected to filer %s grpc address %s", *sftpOpt.filer, filerAddress.ToGrpcAddress())
							 | 
						|
											break
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									go stats_collect.LoopPushingMetric("sftp", stats_collect.SourceName(uint32(*sftpOpt.port)), metricsAddress, metricsIntervalSec)
							 | 
						|
								
							 | 
						|
									// Parse auth methods
							 | 
						|
									var authMethods []string
							 | 
						|
									if *sftpOpt.authMethods != "" {
							 | 
						|
										authMethods = util.StringSplit(*sftpOpt.authMethods, ",")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create a new SFTP service instance with all options
							 | 
						|
									service := sftpd.NewSFTPService(&sftpd.SFTPServiceOptions{
							 | 
						|
										GrpcDialOption:      grpcDialOption,
							 | 
						|
										DataCenter:          *sftpOpt.dataCenter,
							 | 
						|
										FilerGroup:          filerGroup,
							 | 
						|
										Filer:               filerAddress,
							 | 
						|
										SshPrivateKey:       *sftpOpt.sshPrivateKey,
							 | 
						|
										HostKeysFolder:      *sftpOpt.hostKeysFolder,
							 | 
						|
										AuthMethods:         authMethods,
							 | 
						|
										MaxAuthTries:        *sftpOpt.maxAuthTries,
							 | 
						|
										BannerMessage:       *sftpOpt.bannerMessage,
							 | 
						|
										LoginGraceTime:      *sftpOpt.loginGraceTime,
							 | 
						|
										ClientAliveInterval: *sftpOpt.clientAliveInterval,
							 | 
						|
										ClientAliveCountMax: *sftpOpt.clientAliveCountMax,
							 | 
						|
										UserStoreFile:       *sftpOpt.userStoreFile,
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Set up Unix socket if on non-Windows platforms
							 | 
						|
									if runtime.GOOS != "windows" {
							 | 
						|
										localSocket := *sftpOpt.localSocket
							 | 
						|
										if localSocket == "" {
							 | 
						|
											localSocket = fmt.Sprintf("/tmp/seaweedfs-sftp-%d.sock", *sftpOpt.port)
							 | 
						|
										}
							 | 
						|
										if err := os.Remove(localSocket); err != nil && !os.IsNotExist(err) {
							 | 
						|
											glog.Fatalf("Failed to remove %s, error: %s", localSocket, err.Error())
							 | 
						|
										}
							 | 
						|
										go func() {
							 | 
						|
											// start on local unix socket
							 | 
						|
											sftpSocketListener, err := net.Listen("unix", localSocket)
							 | 
						|
											if err != nil {
							 | 
						|
												glog.Fatalf("Failed to listen on %s: %v", localSocket, err)
							 | 
						|
											}
							 | 
						|
											if err := service.Serve(sftpSocketListener); err != nil {
							 | 
						|
												glog.Fatalf("Failed to serve SFTP on socket %s: %v", localSocket, err)
							 | 
						|
											}
							 | 
						|
										}()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Start the SFTP service on TCP
							 | 
						|
									listenAddress := fmt.Sprintf("%s:%d", *sftpOpt.bindIp, *sftpOpt.port)
							 | 
						|
									sftpListener, sftpLocalListener, err := util.NewIpAndLocalListeners(*sftpOpt.bindIp, *sftpOpt.port, time.Duration(10)*time.Second)
							 | 
						|
									if err != nil {
							 | 
						|
										glog.Fatalf("SFTP server listener on %s error: %v", listenAddress, err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(0).Infof("Start Seaweed SFTP Server %s at %s", version.Version(), listenAddress)
							 | 
						|
								
							 | 
						|
									if sftpLocalListener != nil {
							 | 
						|
										go func() {
							 | 
						|
											if err := service.Serve(sftpLocalListener); err != nil {
							 | 
						|
												glog.Fatalf("SFTP Server failed to serve on local listener: %v", err)
							 | 
						|
											}
							 | 
						|
										}()
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := service.Serve(sftpListener); err != nil {
							 | 
						|
										glog.Fatalf("SFTP Server failed to serve: %v", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return true
							 | 
						|
								}
							 |