Go 实现SFTP连接服务

我们将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) {
	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.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服务器。记得在正式环境中处理好错误和日志记录,确保服务的稳定性和安全性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

信息系统项目管理师 | 新一代信息技术

关注WX:CodingTechWork 物联网 定义 The Internet of Things是指通过信息传感设备,按约定的协议,将任何物品与互联网连接,进行信息交互和通信,以实现智能化识别。定位、跟踪、监控和管理的一种网络。物联网主要解决…

采购OLED透明屏指南

一、引言 OLED透明屏作为一种前沿的显示技术,以其独特的透明度和出色的显示效果,受到了众多行业的青睐。在采购OLED透明屏时,需要综合考虑多个因素,以确保选择到符合需求的高质量产品。以下是一份详细的采购OLED透明屏指南&#x…

昇思25天学习打卡营第1天|基本介绍与快速入门

先贴上打卡截图 基本介绍 首先来看基本介绍,昇思MindSpore是华为的一个全场景深度学习框架,属于昇腾AI全栈的一部分。 总体架构如下图所示(来自官方学习材料) 从对底层多样性硬件适用的Runtime到应用层面的Model Zoo、科学计算…

首码项目对接app推广,寻找核心资源项目!

深度挖掘首码网(www.shoumw.com)项编码项目网站:帮助您轻松获取最新项目资源 在当下这个充满机会和挑战的创业氛围中,找到可信赖的项目资源已成为创业者们的主要任务。首码项目网是一家专注于首码项目发布和推广的平台&#xff0c…

Hi3861 OpenHarmony嵌入式应用入门--LiteOS MessageQueue

CMSIS 2.0接口中的消息(Message)功能主要涉及到实时操作系统(RTOS)中的线程间通信。在CMSIS 2.0标准中,消息通常是通过消息队列(MessageQueue)来进行处理的,以实现不同线程之间的信息…

Calibre - 合并电子书(EpubMerge)

这里使用 Calibre 软件和 EpubMerge 插件 EpubMerge github : https://github.com/JimmXinu/EpubMerge 1、安装 Merge 插件 安装后需要重启 calibre 2、查看设置 4 3、选中文件、开始合并 合并完成后,会弹窗窗口,来编辑 合辑的元信息 完成…

全网最强SpringMVC教程 | 万字长文爆肝SpringMVC(二)

SpringMVC_day02 今日内容 完成SSM的整合开发能够理解并实现统一结果封装与统一异常处理能够完成前后台功能整合开发掌握拦截器的编写 1,SSM整合 前面我们已经把Mybatis、Spring和SpringMVC三个框架进行了学习,今天主要的内容就是把这三个框架整合在一…

LED热管理

LED照明系统的热管理 本文提供了用于LED灯具的热管理系统。 包含LED轨道灯具包括照明组件、安装到照明组件上并具有多个孔的夹具壳体,以及将夹具壳体固定到轨道上的安装结构。 照明组件包括具有多个翅片的散热器、安装在所述散热器上的反射器、支撑在所述散热器上…

基于RabbitMQ原理的自定义消息队列实现

文章目录 1. 什么是消息队列2. 需求分析2.1. 核心概念12.2. 核心概念22.3. 核心API2.4. 交换机类型2.5. 持久化2.6. 网络通信2.7. 总结 3. 创建核心类3.1. Exchange3.2. MSGQueue3.3. Binding3.4. Message3.5. 数据库操作3.5.1. 建表操作3.5.2. 交换机操作3.5.3. 队列操作3.5.4…

树莓集团:专业运营,打造理想物业环境

树莓集团通过打造国际化基础物业体系、推进绿色环保理念、空间优化设计和运营、提供一站式服务以及数字化人才培养等措施,致力于提供高品质的物业环境,为企业的发展和创新提供有力的保障和支持。 国际化基础物业体系:树莓集团注重打造高品质的…

今天, 我收到一封勒索邮件, 问我要7w人民币

邮件内容: Hi.This is your last chance to prevent unpleasant consequences and save your reputation. Your operating systems on every device you use to log into your emails are infected with a Trojan virus. I use a multiplatform virus with a hidden VNC. It w…

酒店强心剂——VR智慧酒店上线,史诗级加强入住率

出门在外,什么才是我们最为头疼的问题呢?衣食住行中,住的问题尤其大,尤其是不熟悉当地情况下,预定酒店才是让人头疼的问题。酒店行业该如何化解这一难题呢?VR全景开启智能化酒店宣传获客新模式,…

Unity3D 八叉树划分空间和可视化

也许更好的阅读体验 成果展示 代码 OctreeNode using System.Collections; using System.Collections.Generic; using UnityEngine; public class OctreeNode {//空间内包含的物体public List<GameObject> areaObjects;//空间中心public Vector3 center;//空间大小pub…

OpenAI 收购桌面实时协作公司 Multi;iOS 18 开放 iPhone 镜像测试丨RTE 开发者日报 Vol.231

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real-Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

开发RpcProvider的网络服务

首先更改src的CMakeLists.txt的内容为&#xff1a; #当前目录的所有源文件放入SRC_LIST aux_source_directory(. SRC_LIST)#生成SHARED动态库 #add_library(mprpc SHARED ${SRC_LIST})#由于muduo是静态库&#xff0c;为了使用muduo&#xff0c;将mprpc也生成为静态库 add_libr…

2024最新算法:鹅优化算法(GOOSE Algorithm,GOOSE)求解23个函数,MATLAB代码

一、算法介绍 鹅优化算法&#xff08;GOOSE Algorithm&#xff0c;GOOSE)是2024年提出的一种智能优化算法&#xff0c;该算法从鹅的休息和觅食行为获得灵感&#xff0c;当鹅听到任何奇怪的声音或动作时&#xff0c;它们会发出响亮的声音来唤醒群中的个体&#xff0c;并保证它们…

C++ 基础:指针和引用浅谈

指针 基本概念 在C中&#xff0c;指针是存储其他变量的内存地址的变量。 我们在程序中声明的每个变量在内存中都有一个关联的位置&#xff0c;我们称之为变量的内存地址。 如果我们的程序中有一个变量 var&#xff0c;那么&var 返回它的内存地址。 int main() {int var…

A股3000点下方继续跳水,股民都跌懵了。

今天的A股跌懵了&#xff0c;让人几乎无法呼吸&#xff0c;盘面上出现2个重要信号&#xff0c;不废话&#xff0c;直接说重点&#xff1a; 1、今天两市又跳水了&#xff0c;但绝大多数的个股已经拒绝下跌&#xff0c;市场已然处于一个阶段底部&#xff0c;短线反弹随时可能出现…

昇思25天学习打卡营第1天|新手上路

这里写自定义目录标题 打卡昇思MindSpore扫盲快速入门 打卡 昇思MindSpore扫盲 第一节基本是一个mindspore的科普扫盲。大概介绍一通mindspore的一些架构&#xff0c;feature&#xff0c;以及其对比于其他同类框架的优势。简单扫读了一遍大概有点印象直接跳过。 快速入门 这…

面相对象程序设计

面相对象程序设计包含内容如下 局域网聊天程序设网页浏览器设计电子日历记事本的设计 以其中的一个的报告进行举例 1需求与总体设计 1 1.1需求分析 1 1.2总体设计方案 1 1.2&#xff0e;1系统功能分析以及功能表 1 1.3系统类图的关系以及表之间的联系 2 2详细设计 3 2.1 Manag…