diff options
Diffstat (limited to 'weed/command')
| -rw-r--r-- | weed/command/command.go | 1 | ||||
| -rw-r--r-- | weed/command/filer.go | 27 | ||||
| -rw-r--r-- | weed/command/server.go | 29 | ||||
| -rw-r--r-- | weed/command/sftp.go | 193 |
4 files changed, 250 insertions, 0 deletions
diff --git a/weed/command/command.go b/weed/command/command.go index 33cdb12d1..e3aff4f97 100644 --- a/weed/command/command.go +++ b/weed/command/command.go @@ -43,6 +43,7 @@ var Commands = []*Command{ cmdVersion, cmdVolume, cmdWebDav, + cmdSftp, } type Command struct { diff --git a/weed/command/filer.go b/weed/command/filer.go index d1241999a..05b1e88c7 100644 --- a/weed/command/filer.go +++ b/weed/command/filer.go @@ -35,6 +35,8 @@ var ( filerWebDavOptions WebDavOption filerStartIam *bool filerIamOptions IamOptions + filerStartSftp *bool + filerSftpOptions SftpOptions ) type FilerOptions struct { @@ -141,6 +143,19 @@ func init() { filerStartIam = cmdFiler.Flag.Bool("iam", false, "whether to start IAM service") filerIamOptions.ip = cmdFiler.Flag.String("iam.ip", *f.ip, "iam server http listen ip address") filerIamOptions.port = cmdFiler.Flag.Int("iam.port", 8111, "iam server http listen port") + + filerStartSftp = cmdFiler.Flag.Bool("sftp", false, "whether to start the SFTP server") + filerSftpOptions.port = cmdFiler.Flag.Int("sftp.port", 2022, "SFTP server listen port") + filerSftpOptions.sshPrivateKey = cmdFiler.Flag.String("sftp.sshPrivateKey", "", "path to the SSH private key file for host authentication") + filerSftpOptions.hostKeysFolder = cmdFiler.Flag.String("sftp.hostKeysFolder", "", "path to folder containing SSH private key files for host authentication") + filerSftpOptions.authMethods = cmdFiler.Flag.String("sftp.authMethods", "password,publickey", "comma-separated list of allowed auth methods: password, publickey, keyboard-interactive") + filerSftpOptions.maxAuthTries = cmdFiler.Flag.Int("sftp.maxAuthTries", 6, "maximum number of authentication attempts per connection") + filerSftpOptions.bannerMessage = cmdFiler.Flag.String("sftp.bannerMessage", "SeaweedFS SFTP Server - Unauthorized access is prohibited", "message displayed before authentication") + filerSftpOptions.loginGraceTime = cmdFiler.Flag.Duration("sftp.loginGraceTime", 2*time.Minute, "timeout for authentication") + filerSftpOptions.clientAliveInterval = cmdFiler.Flag.Duration("sftp.clientAliveInterval", 5*time.Second, "interval for sending keep-alive messages") + filerSftpOptions.clientAliveCountMax = cmdFiler.Flag.Int("sftp.clientAliveCountMax", 3, "maximum number of missed keep-alive messages before disconnecting") + filerSftpOptions.userStoreFile = cmdFiler.Flag.String("sftp.userStoreFile", "", "path to JSON file containing user credentials and permissions") + filerSftpOptions.localSocket = cmdFiler.Flag.String("sftp.localSocket", "", "default to /tmp/seaweedfs-sftp-<port>.sock") } func filerLongDesc() string { @@ -235,6 +250,18 @@ func runFiler(cmd *Command, args []string) bool { time.Sleep(delay * time.Second) filerIamOptions.startIamServer() }(startDelay) + startDelay++ + } + + if *filerStartSftp { + sftpOptions.filer = &filerAddress + if *f.dataCenter != "" && *filerSftpOptions.dataCenter == "" { + filerSftpOptions.dataCenter = f.dataCenter + } + go func(delay time.Duration) { + time.Sleep(delay * time.Second) + sftpOptions.startSftpServer() + }(startDelay) } f.masters = pb.ServerAddresses(*f.mastersString).ToServiceDiscovery() diff --git a/weed/command/server.go b/weed/command/server.go index 797cde0dd..dd3b0c8b4 100644 --- a/weed/command/server.go +++ b/weed/command/server.go @@ -28,6 +28,7 @@ var ( masterOptions MasterOptions filerOptions FilerOptions s3Options S3Options + sftpOptions SftpOptions iamOptions IamOptions webdavOptions WebDavOption mqBrokerOptions MessageQueueBrokerOptions @@ -73,6 +74,7 @@ var ( isStartingVolumeServer = cmdServer.Flag.Bool("volume", true, "whether to start volume server") isStartingFiler = cmdServer.Flag.Bool("filer", false, "whether to start filer") isStartingS3 = cmdServer.Flag.Bool("s3", false, "whether to start S3 gateway") + isStartingSftp = cmdServer.Flag.Bool("sftp", false, "whether to start Sftp server") isStartingIam = cmdServer.Flag.Bool("iam", false, "whether to start IAM service") isStartingWebDav = cmdServer.Flag.Bool("webdav", false, "whether to start WebDAV gateway") isStartingMqBroker = cmdServer.Flag.Bool("mq.broker", false, "whether to start message queue broker") @@ -159,6 +161,17 @@ func init() { s3Options.bindIp = cmdServer.Flag.String("s3.ip.bind", "", "ip address to bind to. If empty, default to same as -ip.bind option.") s3Options.idleTimeout = cmdServer.Flag.Int("s3.idleTimeout", 10, "connection idle seconds") + sftpOptions.port = cmdServer.Flag.Int("sftp.port", 2022, "SFTP server listen port") + sftpOptions.sshPrivateKey = cmdServer.Flag.String("sftp.sshPrivateKey", "", "path to the SSH private key file for host authentication") + sftpOptions.hostKeysFolder = cmdServer.Flag.String("sftp.hostKeysFolder", "", "path to folder containing SSH private key files for host authentication") + sftpOptions.authMethods = cmdServer.Flag.String("sftp.authMethods", "password,publickey", "comma-separated list of allowed auth methods: password, publickey, keyboard-interactive") + sftpOptions.maxAuthTries = cmdServer.Flag.Int("sftp.maxAuthTries", 6, "maximum number of authentication attempts per connection") + sftpOptions.bannerMessage = cmdServer.Flag.String("sftp.bannerMessage", "SeaweedFS SFTP Server - Unauthorized access is prohibited", "message displayed before authentication") + sftpOptions.loginGraceTime = cmdServer.Flag.Duration("sftp.loginGraceTime", 2*time.Minute, "timeout for authentication") + sftpOptions.clientAliveInterval = cmdServer.Flag.Duration("sftp.clientAliveInterval", 5*time.Second, "interval for sending keep-alive messages") + sftpOptions.clientAliveCountMax = cmdServer.Flag.Int("sftp.clientAliveCountMax", 3, "maximum number of missed keep-alive messages before disconnecting") + sftpOptions.userStoreFile = cmdServer.Flag.String("sftp.userStoreFile", "", "path to JSON file containing user credentials and permissions") + sftpOptions.localSocket = cmdServer.Flag.String("sftp.localSocket", "", "default to /tmp/seaweedfs-sftp-<port>.sock") iamOptions.port = cmdServer.Flag.Int("iam.port", 8111, "iam server http listen port") webdavOptions.port = cmdServer.Flag.Int("webdav.port", 7333, "webdav server http listen port") @@ -190,6 +203,9 @@ func runServer(cmd *Command, args []string) bool { if *isStartingS3 { *isStartingFiler = true } + if *isStartingSftp { + *isStartingFiler = true + } if *isStartingIam { *isStartingFiler = true } @@ -223,6 +239,9 @@ func runServer(cmd *Command, args []string) bool { if *s3Options.bindIp == "" { s3Options.bindIp = serverBindIp } + if sftpOptions.bindIp == nil || *sftpOptions.bindIp == "" { + sftpOptions.bindIp = serverBindIp + } iamOptions.ip = serverBindIp iamOptions.masters = masterOptions.peers webdavOptions.ipBind = serverBindIp @@ -246,11 +265,13 @@ func runServer(cmd *Command, args []string) bool { mqBrokerOptions.dataCenter = serverDataCenter mqBrokerOptions.rack = serverRack s3Options.dataCenter = serverDataCenter + sftpOptions.dataCenter = serverDataCenter filerOptions.disableHttp = serverDisableHttp masterOptions.disableHttp = serverDisableHttp filerAddress := string(pb.NewServerAddress(*serverIp, *filerOptions.port, *filerOptions.portGrpc)) s3Options.filer = &filerAddress + sftpOptions.filer = &filerAddress iamOptions.filer = &filerAddress webdavOptions.filer = &filerAddress mqBrokerOptions.filerGroup = filerOptions.filerGroup @@ -291,6 +312,14 @@ func runServer(cmd *Command, args []string) bool { }() } + if *isStartingSftp { + go func() { + time.Sleep(2 * time.Second) + sftpOptions.localSocket = filerOptions.localSocket + sftpOptions.startSftpServer() + }() + } + if *isStartingIam { go func() { time.Sleep(2 * time.Second) diff --git a/weed/command/sftp.go b/weed/command/sftp.go new file mode 100644 index 000000000..117f01d6e --- /dev/null +++ b/weed/command/sftp.go @@ -0,0 +1,193 @@ +package command + +import ( + "context" + "fmt" + "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", util.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 +} |
