我们将SFTP连接和处理逻辑,以及登录账户信息封装,这样可以在不同的地方重用代码,并且可以轻松地更改登录凭据。下面我将演示如何使用Go语言中的结构体来封装这些信息,并实现一个简单的SFTP服务器:
package mainimport ("errors""fmt""io""io/ioutil""log""net""os""path/filepath""strings""github.com/pkg/sftp""golang.org/x/crypto/ssh"
)// 实现自定义请求处理程序
type CustomHandler struct {baseDir string // 基础目录,用于限制SFTP操作在特定目录下
}func (h *CustomHandler) Fileread(request *sftp.Request) (io.ReaderAt, error) {path := filepath.Join(h.baseDir, request.Filepath)file, err := os.Open(path)if err != nil {return nil, err}return file, nil
}func (h *CustomHandler) Filewrite(request *sftp.Request) (io.WriterAt, error) {path := filepath.Join(h.baseDir, request.Filepath)file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)if err != nil {return nil, err}return file, nil
}func (h *CustomHandler) Filecmd(request *sftp.Request) error {path := filepath.Join(h.baseDir, request.Filepath)switch request.Method {case "Rename":// 对于重命名,request.Target 会包含新的文件名targetPath := filepath.Join(h.baseDir, request.Target)// 确保目标路径不在 baseDir 之外if !strings.HasPrefix(targetPath, h.baseDir) {return errors.New("invalid target path")}return os.Rename(path, targetPath)case "Rmdir":// 删除目录return os.Remove(path)case "Mkdir":// 创建目录return os.Mkdir(path, os.ModePerm)case "Remove":return os.Remove(path)case "Setstat", "Link", "Symlink":fallthroughdefault:log.Printf("Filecmd request %v", request)return errors.New("operation not supported")}return nil
}func (h *CustomHandler) Filelist(request *sftp.Request) (sftp.ListerAt, error) {path := filepath.Join(h.baseDir, request.Filepath)switch request.Method {case "List":// 检索目录内容files, err := ioutil.ReadDir(path)if err != nil {return nil, err}// 将 os.FileInfo 列表转换为 sftp.ListerAtreturn listerAt(files), nilcase "Stat":info, err := os.Stat(path)if err != nil {return nil, err}return listerAt([]os.FileInfo{info}), nilcase "Readlink":target, err := os.Readlink(path)if err != nil {return nil, err}info, err := os.Lstat(target)if err != nil {return nil, err}return listerAt([]os.FileInfo{info}), nil}return nil, nil
}// listerAt 是一个辅助类型,用于实现 sftp.ListerAt 接口
type listerAt []os.FileInfofunc (l listerAt) ListAt(list []os.FileInfo, offset int64) (int, error) {if offset >= int64(len(l)) {return 0, io.EOF}n := copy(list, l[offset:])return n, nil
}type SFTPServer struct {HostKeyPath stringAuthUser stringAuthPass stringPort string
}func NewSFTPServer(hostKeyPath, user, pass, port string) *SFTPServer {return &SFTPServer{HostKeyPath: hostKeyPath,AuthUser: user,AuthPass: pass,Port: port,}
}func (server *SFTPServer) Start() {config := &ssh.ServerConfig{PasswordCallback: server.passwordCallback,}privateBytes, err := os.ReadFile(server.HostKeyPath)if err != nil {log.Fatalf("Failed to load host key: %v", err)}private, err := ssh.ParsePrivateKey(privateBytes)if err != nil {log.Fatalf("Failed to parse host key: %v", err)}config.AddHostKey(private)listener, err := net.Listen("tcp", "0.0.0.0:"+server.Port)if err != nil {log.Fatalf("Failed to listen on port %s: %v", server.Port, err)}log.Printf("Listening on port %s...", server.Port)for {conn, err := listener.Accept()if err != nil {log.Printf("Failed to accept incoming connection: %v", err)continue}go server.handleConn(conn, config)}
}func (server *SFTPServer) handleConn(nConn net.Conn, config *ssh.ServerConfig) {sshConn, chans, reqs, err := ssh.NewServerConn(nConn, config)if err != nil {log.Printf("Failed to handshake: %v", err)return}defer sshConn.Close()go ssh.DiscardRequests(reqs)for newChannel := range chans {if newChannel.ChannelType() == "session" {go server.handleChannel(newChannel)} else {newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")}}
}func (server *SFTPServer) handleChannel(newChannel ssh.NewChannel) {if newChannel.ChannelType() != "session" {newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")return}channel, requests, err := newChannel.Accept()if err != nil {log.Printf("Could not accept channel (%s)", err)return}defer channel.Close()// 只在需要时创建 SFTP 服务器实例var sftpServer *sftp.Serverfor req := range requests {switch req.Type {case "subsystem":if string(req.Payload[4:]) == "sftp" {req.Reply(true, nil)baseDir, err := server.getWorkDir()if err != nil {log.Printf("Failed to create SFTP work dir: %v", err)return}handler := &CustomHandler{baseDir: baseDir,}sftpServer := sftp.NewRequestServer(channel, sftp.Handlers{FileGet: handler,FilePut: handler,FileCmd: handler,FileList: handler,})if err := sftpServer.Serve(); err == io.EOF {log.Printf("SFTP client disconnected")return} else if err != nil {log.Printf("SFTP server completed with error: %v", err)return}} else {req.Reply(false, nil)}default:req.Reply(false, nil)}}if sftpServer == nil {log.Printf("No SFTP subsystem started")return}
}func (server *SFTPServer) passwordCallback(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {if c.User() == server.AuthUser && string(pass) == server.AuthPass {return nil, nil}return nil, fmt.Errorf("password rejected for %q", c.User())
}func (server *SFTPServer) getWorkDir() (string, error) {// 获取当前工作目录workingDir, err := os.Getwd()if err != nil {log.Fatalf("Unable to get current working directory: %v", err)return "", err}// 构建操作目录的完整路径baseDir := filepath.Join(workingDir, "sftp_tmp")// 确保目录存在err = os.MkdirAll(baseDir, os.ModePerm)if err != nil {log.Fatalf("Unable to create tmp directory: %v", err)return "", err}return baseDir, nil
}func main() {sftpServer := NewSFTPServer("~/.ssh/id_rsa", "root", "123456", "9527")sftpServer.Start()
}
效果图:
在这个封装中,SFTPServer
结构体包含SSH服务器的配置信息,如主机密钥路径、授权用户名、密码和监听端口。NewSFTPServer
函数是构造函数,用于创建SFTPServer
实例。Start
方法启动SFTP服务器并监听指定端口。
注意:请确保替换 hostKeyPath
、username
、password
和 port
这些参数为您自己的设置。~/.ssh/id_rsa
是您的服务器私钥文件的路径,您需要将其替换为实际的文件路径。root
和 123456
是您希望用户使用的登录凭证。9527
是您希望SFTP服务器监听的端口号。
在上述封装代码中,Start
方法会启动SFTP服务器并等待连接。对于每个新连接,都会在一个新的goroutine中调用 handleConn
方法,进行SSH握手并处理SFTP会话。handleConn
方法会处理新通道,并将每个新通道传递给 handleChannel
方法,后者配置并启动SFTP服务。
passwordCallback
方法是一个回调函数,用于在SSH握手过程中验证用户凭证。如果提供的用户名和密码与结构体中定义的匹配,则会允许连接。
最后,main
函数实例化了 SFTPServer
并启动了SFTP服务。您需要确保您的系统中有SSH私钥文件,并且您有权使用指定的端口。
在实际部署中,您应该使用更安全的方法存储用户凭据,例如使用加密的方式,或者通过集成现有的用户管理系统,而不是将用户名和密码硬编码在代码中。
此外,您可能还需要添加更多的功能,例如支持基于公钥的认证、限制用户的文件系统访问权限、记录日志到文件等。这些功能可以根据需要扩展SFTPServer
结构体和相关方法。
在您的main
函数中调用sftpServer.Start()
,就可以启动SFTP服务器。记得在正式环境中处理好错误和日志记录,确保服务的稳定性和安全性。