Go 实现SFTP连接服务

news2024/10/7 18:20:01

我们将SFTP连接和处理逻辑,以及登录账户信息封装,这样可以在不同的地方重用代码,并且可以轻松地更改登录凭据。下面我将演示如何使用Go语言中的结构体来封装这些信息,并实现一个简单的SFTP服务器:

package main

import (
	"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":
		fallthrough
	default:
		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.ListerAt
		return listerAt(files), nil
	case "Stat":
		info, err := os.Stat(path)
		if err != nil {
			return nil, err
		}
		return listerAt([]os.FileInfo{info}), nil
	case "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.FileInfo

func (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 string
	AuthUser    string
	AuthPass    string
	Port        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) {
	channel, requests, err := newChannel.Accept()
	if err != nil {
		log.Printf("Could not accept channel (%s)", err)
		return
	}
	defer channel.Close()

	// 只在需要时创建 SFTP 服务器实例
	var sftpServer *sftp.Server

	for 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服务器并监听指定端口。

注意:请确保替换 hostKeyPathusernamepasswordport 这些参数为您自己的设置。~/.ssh/id_rsa 是您的服务器私钥文件的路径,您需要将其替换为实际的文件路径。root123456 是您希望用户使用的登录凭证。9527 是您希望SFTP服务器监听的端口号。

在上述封装代码中,Start 方法会启动SFTP服务器并等待连接。对于每个新连接,都会在一个新的goroutine中调用 handleConn 方法,进行SSH握手并处理SFTP会话。handleConn 方法会处理新通道,并将每个新通道传递给 handleChannel 方法,后者配置并启动SFTP服务。

passwordCallback 方法是一个回调函数,用于在SSH握手过程中验证用户凭证。如果提供的用户名和密码与结构体中定义的匹配,则会允许连接。

最后,main 函数实例化了 SFTPServer 并启动了SFTP服务。您需要确保您的系统中有SSH私钥文件,并且您有权使用指定的端口。

在实际部署中,您应该使用更安全的方法存储用户凭据,例如使用加密的方式,或者通过集成现有的用户管理系统,而不是将用户名和密码硬编码在代码中。

此外,您可能还需要添加更多的功能,例如支持基于公钥的认证、限制用户的文件系统访问权限、记录日志到文件等。这些功能可以根据需要扩展SFTPServer结构体和相关方法。

在您的main函数中调用sftpServer.Start(),就可以启动SFTP服务器。记得在正式环境中处理好错误和日志记录,确保服务的稳定性和安全性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1874713.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

亚太杯赛题思路发布(中文版)

导读: 本文将继续修炼回归模型算法,并总结了一些常用的除线性回归模型之外的模型,其中包括一些单模型及集成学习器。 保序回归、多项式回归、多输出回归、多输出K近邻回归、决策树回归、多输出决策树回归、AdaBoost回归、梯度提升决策树回归…

3d模型里地毯的材质怎么赋予?---模大狮模型网

在进行3D建模时,赋予地毯逼真的材质是营造现实感和增强场景氛围的重要步骤。模大狮将介绍在常见的3D建模软件中,如何有效地为地毯赋予各种材质,以及一些实用的技巧和注意事项。 一、选择合适的地毯材质 在3D建模中,地毯的材质选择…

【ai】tx2 nx: trition client安装nvidia-pyindex 一直失败

系统版本的pip和python虚拟环境的pipyolov4-triton-tensorrt的master分支 官方client jetson:pip3 install --user nvidia-pyindex 不成功啊 这个是让nvidia-pyindex 拉取nvidia@tx2-nx:~$ pip3 install --user nvidia-pyindex Collecting nvidia-pyindexDownloading https://…

center()方法——字符串居中填充

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法参考 字符串对象的center()方法用于将字符串填充至指定长度,并将原字符串居中输出。center()方法的语法格式如下: str.…

用Python将PowerPoint演示文稿转换到图片和SVG

PowerPoint演示文稿作为展示创意、分享知识和表达观点的重要工具,被广泛应用于教育、商务汇报及个人项目展示等领域。然而,面对不同的分享场景与接收者需求,有时需要我们将PPT内容以图片形式保存与传播。这样能够避免软件兼容性的限制&#x…

使用Colly库进行高效的网络爬虫开发

引言 随着互联网技术的飞速发展,网络数据已成为信息获取的重要来源。网络爬虫作为自动获取网页内容的工具,在数据分析、市场研究、信息聚合等领域发挥着重要作用。本文将介绍如何使用Go语言中的Colly库来开发高效的网络爬虫。 什么是Colly库&#xff1…

笔记本电脑为什么可以链接热点,却无法连接WiFi

① 在开始菜单的搜索栏中,输入 cmd 。 ② 右击上方该程序,选择 以管理员身份运行 ③ 输入:nestsh winsock reset ④ 敲击回车,显示如下页面 ⑤ 再输入 ipconfig/flushdns 回车 ⑥ 然后重启电脑,OVER!

MySQL高级-SQL优化- count 优化 - 尽量使用count(*)

文章目录 1、count 优化2、count的几种用法3、count(*)4、count(id)5、count(profession)6、count(null)7、 count(1) 1、count 优化 MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高&a…

【python】socket通信代码解析

目录 一、socket通信原理 1.1 服务器端 1.2 客户端 二、socket通信主要应用场景 2.1 简单的服务器和客户端通信 2.2 并发服务器 2.3 UDP通信 2.4 文件传输 2.5 HTTP服务器 2.6 邮件发送与接收 2.7 FTP客户端 2.8 P2P文件共享 2.9 网络游戏 三、python中Socket编…

信息学奥赛初赛天天练-38-CSP-J2021阅读程序-约数个数、约数和、埃氏筛法、欧拉筛法筛素数应用

PDF文档公众号回复关键字:20240628 2021 CSP-J 阅读程序3 1阅读程序(判断题1.5分 选择题3分 共计40分 ) 01 #include<stdio.h> 02 using namespace std; 03 04 #define n 100000 05 #define N n1 06 07 int m; 08 int a[N],b[N],c[N],d[N]; 09 int f[N],g[N]; 10 11 …

Linux操作系统学习:day07

内容来自&#xff1a;Linux介绍 视频推荐&#xff1a;[Linux基础入门教程-linux命令-vim-gcc/g -动态库/静态库 -makefile-gdb调试]( 目录 day0742、使用 grep 搜索文件内容43、使用 locate 搜索文件44、 vim 的安装和介绍vim的模式 45、命令模式下光标的移动1、保存退出2、代…

昇思MindSpore学习总结四——数据变换Transforms

1、数据变换 数据变换&#xff0c;字面意思&#xff0c;就是将我们在实际项目中获取的数据进行相应的操作&#xff0c;方便后期处理。数据变换的方法很多&#xff0c;例如归一化、标准化等。 为什么要进行数据变换&#xff1f;&#xff08;1&#xff09;我们采集到的数据&#…

Linux——echo命令,管道符,vi/vim 文本编辑器

1.echo 命令 作用 向终端设备上输出字符串或变量的存储数据 格式 echo " 字符串 " echo $ 变 量名 [rootserver ~] # echo $SHELL # 输出变量的值必须加 $ /bin/bash [rootserver ~] # str1" 我爱中国 " # 自定义变量 echo 重定向输出到文件 ec…

简单的本地局域网的前后端接口联调

由于项目被赶进度了&#xff0c;急于前后端联调接口&#xff0c;但是我又没钱买服务器&#xff08;主要我也不会部署&#xff09;&#xff0c;所以我这里就紧急找一个后端的大神朋友请教了一下&#xff1a;苏泽SuZe-CSDN博客 提示&#xff1a;这里不讲后端怎么写接口、前端怎么…

SBTI(科学碳目标)认证是什么?

SBTI认证&#xff0c;全称为“科学基础目标设置倡议”&#xff08;Science-Based Targets initiative&#xff09;认证&#xff0c;是一种广泛认可的企业可持续发展标准。以下是关于SBTI认证的详细解释&#xff1a; 一、认证目标 SBTI认证旨在推动企业采取可持续的经营实践&a…

云原生之使用Docker部署RabbitMQ消息中间件

云原生之使用Docker部署RabbitMQ消息中间件 一、RabbitMQ介绍1.1 RabbitMQ简介1.2 RabbitMQ特点1.3 RabbitMQ使用场景 二、检查Docker环境2.1 检查Docker版本2.2 检查操作系统版本2.3 检查Docker状态 三、下载RabbitMQ镜像四、部署RabbitMQ服务4.1创建挂载目录4.2 运行RabbitMQ…

利用代理IP实现高效大数据抓取的策略与技巧

在当今信息爆炸的时代&#xff0c;数据对于各行各业都至关重要。而数据的获取往往需要通过网络爬取。然而随着网络安全意识的提高和反爬虫机制的加强&#xff0c;传统的数据爬取方式可能会受到限制。在这种情况下&#xff0c;代理IP技术的应用就显得尤为重要。本文将探讨代理IP…

S32K3 --- Wdg(内狗) Mcal配置

前言 看门狗的作用是用来检测程序是否跑飞,进入死循环。我们需要不停地喂狗,来确保程序是正常运行的,一旦停止喂狗,意味着程序跑飞,超时后就会reset复位程序。 一、Wdg 1.1 WdgGeneral Wdg Disable Allowed : 启用此参数后,允许在运行的时候禁用看门狗 Wdg Enable User…

服务器日志事件ID4107:从自动更新 cab 中提取第三方的根目录列表失败,错误为: 已处理证书链,但是在不受信任提供程序信任的根证书中终止。

在查看Windows系统日志时&#xff0c;你是否有遇到过事件ID4107错误&#xff0c;来源CAPI2&#xff0c;详细信息在 http://www.download.windowsupdate.com/msdownload/update/v3/static/trustedr/en/authrootstl.cab 从自动更新 cab 中提取第三方的根目录列表失败&#xff0c;…

使用自定义的shiro密码匹配器CredentialsMatcher完成密码验证

今天突然想研究一下shiro怎么匹配用户的密码。 我们使用shiro的API登录时&#xff0c;会先创建一个令牌对象&#xff0c;而经常用的令牌对象是UsernamePasswordToken&#xff0c;把用户输入的用户名和密码作为参数构建一个UsernamePasswordToken&#xff0c;然后通过Subject.l…