sftp_service.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. // sftp_service.go
  2. package sftpd
  3. import (
  4. "context"
  5. "fmt"
  6. "io"
  7. "net"
  8. "os"
  9. "path/filepath"
  10. "time"
  11. "github.com/pkg/sftp"
  12. "github.com/seaweedfs/seaweedfs/weed/glog"
  13. "github.com/seaweedfs/seaweedfs/weed/pb"
  14. "github.com/seaweedfs/seaweedfs/weed/sftpd/auth"
  15. "github.com/seaweedfs/seaweedfs/weed/sftpd/user"
  16. "golang.org/x/crypto/ssh"
  17. "google.golang.org/grpc"
  18. )
  19. // SFTPService holds configuration for the SFTP service.
  20. type SFTPService struct {
  21. options SFTPServiceOptions
  22. userStore user.Store
  23. authManager *auth.Manager
  24. }
  25. // SFTPServiceOptions contains all configuration options for the SFTP service.
  26. type SFTPServiceOptions struct {
  27. GrpcDialOption grpc.DialOption
  28. DataCenter string
  29. FilerGroup string
  30. Filer pb.ServerAddress
  31. // SSH Configuration
  32. SshPrivateKey string // Legacy single host key
  33. HostKeysFolder string // Multiple host keys for different algorithms
  34. AuthMethods []string // Enabled auth methods: "password", "publickey", "keyboard-interactive"
  35. MaxAuthTries int // Limit authentication attempts
  36. BannerMessage string // Pre-auth banner message
  37. LoginGraceTime time.Duration // Timeout for authentication
  38. // Connection Management
  39. ClientAliveInterval time.Duration // Keep-alive check interval
  40. ClientAliveCountMax int // Max missed keep-alives before disconnect
  41. // User Management
  42. UserStoreFile string // Path to user store file
  43. }
  44. // NewSFTPService creates a new service instance.
  45. func NewSFTPService(options *SFTPServiceOptions) *SFTPService {
  46. service := SFTPService{options: *options}
  47. // Initialize user store
  48. userStore, err := user.NewFileStore(options.UserStoreFile)
  49. if err != nil {
  50. glog.Fatalf("Failed to initialize user store: %v", err)
  51. }
  52. service.userStore = userStore
  53. // Initialize auth manager
  54. service.authManager = auth.NewManager(userStore, options.AuthMethods)
  55. return &service
  56. }
  57. // Serve accepts incoming connections on the provided listener and handles them.
  58. func (s *SFTPService) Serve(listener net.Listener) error {
  59. // Build SSH server config
  60. sshConfig, err := s.buildSSHConfig()
  61. if err != nil {
  62. return fmt.Errorf("failed to create SSH config: %w", err)
  63. }
  64. glog.V(0).Infof("Starting Seaweed SFTP service on %s", listener.Addr().String())
  65. for {
  66. conn, err := listener.Accept()
  67. if err != nil {
  68. return fmt.Errorf("failed to accept incoming connection: %w", err)
  69. }
  70. go s.handleSSHConnection(conn, sshConfig)
  71. }
  72. }
  73. // buildSSHConfig creates the SSH server configuration with proper authentication.
  74. func (s *SFTPService) buildSSHConfig() (*ssh.ServerConfig, error) {
  75. // Get base config from auth manager
  76. config := s.authManager.GetSSHServerConfig()
  77. // Set additional options
  78. config.MaxAuthTries = s.options.MaxAuthTries
  79. config.BannerCallback = func(conn ssh.ConnMetadata) string {
  80. return s.options.BannerMessage
  81. }
  82. config.ServerVersion = "SSH-2.0-SeaweedFS-SFTP" // Custom server version
  83. hostKeysAdded := 0
  84. // Add legacy host key if specified
  85. if s.options.SshPrivateKey != "" {
  86. if err := s.addHostKey(config, s.options.SshPrivateKey); err != nil {
  87. return nil, err
  88. }
  89. hostKeysAdded++
  90. }
  91. // Add all host keys from the specified folder
  92. if s.options.HostKeysFolder != "" {
  93. files, err := os.ReadDir(s.options.HostKeysFolder)
  94. if err != nil {
  95. return nil, fmt.Errorf("failed to read host keys folder: %w", err)
  96. }
  97. for _, file := range files {
  98. if file.IsDir() {
  99. continue // Skip directories
  100. }
  101. keyPath := filepath.Join(s.options.HostKeysFolder, file.Name())
  102. if err := s.addHostKey(config, keyPath); err != nil {
  103. // Log the error but continue with other keys
  104. glog.V(0).Info(fmt.Sprintf("Failed to add host key %s: %v", keyPath, err))
  105. continue
  106. }
  107. hostKeysAdded++
  108. }
  109. if hostKeysAdded == 0 {
  110. glog.V(0).Info(fmt.Sprintf("Warning: no valid host keys found in folder %s", s.options.HostKeysFolder))
  111. }
  112. }
  113. // Ensure we have at least one host key
  114. if hostKeysAdded == 0 {
  115. return nil, fmt.Errorf("no host keys provided")
  116. }
  117. return config, nil
  118. }
  119. // addHostKey adds a host key to the SSH server configuration.
  120. func (s *SFTPService) addHostKey(config *ssh.ServerConfig, keyPath string) error {
  121. keyBytes, err := os.ReadFile(keyPath)
  122. if err != nil {
  123. return fmt.Errorf("failed to read host key %s: %v", keyPath, err)
  124. }
  125. // Try parsing as private key
  126. signer, err := ssh.ParsePrivateKey(keyBytes)
  127. if err != nil {
  128. // Try parsing with passphrase if available
  129. if passphraseErr, ok := err.(*ssh.PassphraseMissingError); ok {
  130. return fmt.Errorf("host key %s requires passphrase: %v", keyPath, passphraseErr)
  131. }
  132. return fmt.Errorf("failed to parse host key %s: %v", keyPath, err)
  133. }
  134. config.AddHostKey(signer)
  135. glog.V(0).Infof("Added host key %s (%s)", keyPath, signer.PublicKey().Type())
  136. return nil
  137. }
  138. // handleSSHConnection handles an incoming SSH connection.
  139. func (s *SFTPService) handleSSHConnection(conn net.Conn, config *ssh.ServerConfig) {
  140. // Set connection deadline for handshake
  141. _ = conn.SetDeadline(time.Now().Add(s.options.LoginGraceTime))
  142. // Perform SSH handshake
  143. sshConn, chans, reqs, err := ssh.NewServerConn(conn, config)
  144. if err != nil {
  145. glog.Errorf("Failed to handshake: %v", err)
  146. conn.Close()
  147. return
  148. }
  149. // Clear deadline after successful handshake
  150. _ = conn.SetDeadline(time.Time{})
  151. // Set up connection monitoring
  152. ctx, cancel := context.WithCancel(context.Background())
  153. defer cancel()
  154. // Start keep-alive monitoring
  155. go s.monitorConnection(ctx, sshConn)
  156. username := sshConn.Permissions.Extensions["username"]
  157. glog.V(0).Infof("New SSH connection from %s (%s) as user %s",
  158. sshConn.RemoteAddr(), sshConn.ClientVersion(), username)
  159. // Get user from store
  160. sftpUser, err := s.authManager.GetUser(username)
  161. if err != nil {
  162. glog.Errorf("Failed to retrieve user %s: %v", username, err)
  163. sshConn.Close()
  164. return
  165. }
  166. // Create user-specific filesystem
  167. userFs := NewSftpServer(
  168. s.options.Filer,
  169. s.options.GrpcDialOption,
  170. s.options.DataCenter,
  171. s.options.FilerGroup,
  172. sftpUser,
  173. )
  174. // Ensure home directory exists with proper permissions
  175. if err := userFs.EnsureHomeDirectory(); err != nil {
  176. glog.Errorf("Failed to ensure home directory for user %s: %v", username, err)
  177. // We don't close the connection here, as the user might still be able to access other directories
  178. }
  179. // Handle SSH requests and channels
  180. go ssh.DiscardRequests(reqs)
  181. for newChannel := range chans {
  182. go s.handleChannel(newChannel, &userFs)
  183. }
  184. }
  185. // monitorConnection monitors an SSH connection with keep-alives.
  186. func (s *SFTPService) monitorConnection(ctx context.Context, sshConn *ssh.ServerConn) {
  187. if s.options.ClientAliveInterval <= 0 {
  188. return
  189. }
  190. ticker := time.NewTicker(s.options.ClientAliveInterval)
  191. defer ticker.Stop()
  192. missedCount := 0
  193. for {
  194. select {
  195. case <-ctx.Done():
  196. return
  197. case <-ticker.C:
  198. // Send keep-alive request
  199. _, _, err := sshConn.SendRequest("keepalive@openssh.com", true, nil)
  200. if err != nil {
  201. missedCount++
  202. glog.V(0).Infof("Keep-alive missed for %s: %v (%d/%d)",
  203. sshConn.RemoteAddr(), err, missedCount, s.options.ClientAliveCountMax)
  204. if missedCount >= s.options.ClientAliveCountMax {
  205. glog.Warningf("Closing unresponsive connection from %s", sshConn.RemoteAddr())
  206. sshConn.Close()
  207. return
  208. }
  209. } else {
  210. missedCount = 0
  211. }
  212. }
  213. }
  214. }
  215. // handleChannel handles a single SSH channel.
  216. func (s *SFTPService) handleChannel(newChannel ssh.NewChannel, fs *SftpServer) {
  217. if newChannel.ChannelType() != "session" {
  218. _ = newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
  219. return
  220. }
  221. channel, requests, err := newChannel.Accept()
  222. if err != nil {
  223. glog.Errorf("Could not accept channel: %v", err)
  224. return
  225. }
  226. go func(in <-chan *ssh.Request) {
  227. for req := range in {
  228. switch req.Type {
  229. case "subsystem":
  230. // Check that the subsystem is "sftp".
  231. if string(req.Payload[4:]) == "sftp" {
  232. _ = req.Reply(true, nil)
  233. s.handleSFTP(channel, fs)
  234. } else {
  235. _ = req.Reply(false, nil)
  236. }
  237. default:
  238. _ = req.Reply(false, nil)
  239. }
  240. }
  241. }(requests)
  242. }
  243. // handleSFTP starts the SFTP server on the SSH channel.
  244. func (s *SFTPService) handleSFTP(channel ssh.Channel, fs *SftpServer) {
  245. // Create server options with initial working directory set to user's home
  246. serverOptions := sftp.WithStartDirectory(fs.user.HomeDir)
  247. server := sftp.NewRequestServer(channel, sftp.Handlers{
  248. FileGet: fs,
  249. FilePut: fs,
  250. FileCmd: fs,
  251. FileList: fs,
  252. }, serverOptions)
  253. if err := server.Serve(); err == io.EOF {
  254. server.Close()
  255. glog.V(0).Info("SFTP client exited session.")
  256. } else if err != nil {
  257. glog.Errorf("SFTP server finished with error: %v", err)
  258. }
  259. }