aboutsummaryrefslogtreecommitdiff
path: root/weed/command/sftp.go
blob: ed93e44fef8b2569f9acfa1401b334910d03c91d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
}