1f7c491048
- 实现文件系统日志(FilesystemLog)记录文件管理器操作 - 实现操作日志(OperationLog)记录用户操作行为 - 实现数据库SQL日志(DatabaseSQLLog)模型和API - 实现SSH会话命令记录(SessionCommand)含命令输出和风险等级 - 添加IP提取服务支持X-Real-IP和X-Forwarded-For - 添加日志自动清理功能 - 修复ProFormSwitch required验证问题 - 修复设置页面默认值问题 - 修复文件上传错误检测逻辑 - 修复资产树key前缀问题 - 添加VNC/RDP设置默认值 - 修复文件管理标题翻译
479 lines
13 KiB
Go
479 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"next-terminal/server/common"
|
|
"next-terminal/server/common/maps"
|
|
"next-terminal/server/global/session"
|
|
"next-terminal/server/log"
|
|
"next-terminal/server/model"
|
|
"next-terminal/server/repository"
|
|
"next-terminal/server/utils"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/pkg/sftp"
|
|
)
|
|
|
|
type FileSystemApi struct{}
|
|
|
|
func (api FileSystemApi) recordFilesystemLog(ctx context.Context, sessionId, action, fileName string) {
|
|
sess, err := repository.SessionRepository.FindById(ctx, sessionId)
|
|
if err != nil {
|
|
log.Error("Failed to find session for filesystem log", log.NamedError("err", err))
|
|
return
|
|
}
|
|
|
|
filesystemLog := &model.FilesystemLog{
|
|
ID: utils.UUID(),
|
|
AssetId: sess.AssetId,
|
|
SessionId: sessionId,
|
|
UserId: sess.Creator,
|
|
Action: action,
|
|
FileName: fileName,
|
|
Created: common.NowJsonTime(),
|
|
}
|
|
|
|
if err := repository.FilesystemLogRepository.Create(ctx, filesystemLog); err != nil {
|
|
log.Error("Failed to create filesystem log", log.NamedError("err", err))
|
|
}
|
|
}
|
|
|
|
func (api FileSystemApi) LsEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
dir := c.QueryParam("dir")
|
|
hiddenFileVisible := c.QueryParam("hiddenFileVisible") == "true"
|
|
|
|
log.Debug("FileSystem Ls: sessionId=" + sessionId + " dir=" + dir)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
log.Debug("FileSystem Ls: failed to create SFTP client: " + err.Error())
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
files, err := sftpClient.ReadDir(dir)
|
|
if err != nil {
|
|
log.Debug("FileSystem Ls: failed to read directory: " + err.Error())
|
|
return Fail(c, -1, "failed to read directory: "+err.Error())
|
|
}
|
|
|
|
var result []maps.Map
|
|
for _, f := range files {
|
|
name := f.Name()
|
|
if !hiddenFileVisible && strings.HasPrefix(name, ".") {
|
|
continue
|
|
}
|
|
result = append(result, maps.Map{
|
|
"name": name,
|
|
"size": f.Size(),
|
|
"modTime": f.ModTime().UnixMilli(),
|
|
"path": path.Join(dir, name),
|
|
"mode": f.Mode().String(),
|
|
"isDir": f.IsDir(),
|
|
"isLink": f.Mode()&os.ModeSymlink != 0,
|
|
})
|
|
}
|
|
|
|
return Success(c, result)
|
|
}
|
|
|
|
func (api FileSystemApi) MkdirEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
dir := c.QueryParam("dir")
|
|
|
|
log.Debug("FileSystem Mkdir: sessionId=" + sessionId + " dir=" + dir)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
err = sftpClient.MkdirAll(dir)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create directory: "+err.Error())
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "mkdir", dir)
|
|
|
|
return Success(c, nil)
|
|
}
|
|
|
|
func (api FileSystemApi) TouchEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
filename := c.QueryParam("filename")
|
|
|
|
log.Debug("FileSystem Touch: sessionId=" + sessionId + " filename=" + filename)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
f, err := sftpClient.Create(filename)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create file: "+err.Error())
|
|
}
|
|
f.Close()
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "touch", filename)
|
|
|
|
return Success(c, nil)
|
|
}
|
|
|
|
func (api FileSystemApi) RmEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
filename := c.QueryParam("filename")
|
|
|
|
log.Debug("FileSystem Rm: sessionId=" + sessionId + " filename=" + filename)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
f, err := sftpClient.Stat(filename)
|
|
if err != nil {
|
|
return Fail(c, -1, "file not found: "+err.Error())
|
|
}
|
|
|
|
if f.IsDir() {
|
|
err = sftpClient.RemoveDirectory(filename)
|
|
if err != nil {
|
|
err = api.removeRecursive(sftpClient, filename)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to remove directory: "+err.Error())
|
|
}
|
|
}
|
|
} else {
|
|
err = sftpClient.Remove(filename)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to remove file: "+err.Error())
|
|
}
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "rm", filename)
|
|
|
|
return Success(c, nil)
|
|
}
|
|
|
|
func (api FileSystemApi) removeRecursive(sftpClient *sftp.Client, dirPath string) error {
|
|
files, err := sftpClient.ReadDir(dirPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, f := range files {
|
|
fullPath := path.Join(dirPath, f.Name())
|
|
if f.IsDir() {
|
|
err = api.removeRecursive(sftpClient, fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err = sftpClient.Remove(fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return sftpClient.RemoveDirectory(dirPath)
|
|
}
|
|
|
|
func (api FileSystemApi) RenameEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
oldName := c.QueryParam("oldName")
|
|
newName := c.QueryParam("newName")
|
|
|
|
log.Debug("FileSystem Rename: sessionId=" + sessionId + " oldName=" + oldName + " newName=" + newName)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
err = sftpClient.Rename(oldName, newName)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to rename: "+err.Error())
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "rename", oldName+" -> "+newName)
|
|
|
|
return Success(c, nil)
|
|
}
|
|
|
|
func (api FileSystemApi) EditEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
|
|
var req struct {
|
|
Filename string `json:"filename"`
|
|
FileContent string `json:"fileContent"`
|
|
}
|
|
if err := c.Bind(&req); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("FileSystem Edit: sessionId=" + sessionId + " filename=" + req.Filename)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
f, err := sftpClient.OpenFile(req.Filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to open file: "+err.Error())
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.Write([]byte(req.FileContent))
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to write file: "+err.Error())
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "edit", req.Filename)
|
|
|
|
return Success(c, nil)
|
|
}
|
|
|
|
func (api FileSystemApi) ReadEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
filename := c.QueryParam("filename")
|
|
|
|
log.Debug("FileSystem Read: sessionId=" + sessionId + " filename=" + filename)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
f, err := sftpClient.Open(filename)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to open file: "+err.Error())
|
|
}
|
|
defer f.Close()
|
|
|
|
content, err := readFileContent(f, 10*1024*1024)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to read file: "+err.Error())
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "read", filename)
|
|
|
|
return Success(c, maps.Map{
|
|
"content": content,
|
|
"path": filename,
|
|
})
|
|
}
|
|
|
|
func readFileContent(f *sftp.File, maxSize int64) (string, error) {
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if stat.Size() > maxSize {
|
|
return "", fmt.Errorf("file too large (max %d bytes)", maxSize)
|
|
}
|
|
|
|
buf := make([]byte, stat.Size())
|
|
_, err = f.Read(buf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(buf), nil
|
|
}
|
|
|
|
func (api FileSystemApi) ChmodEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
filename := c.QueryParam("filename")
|
|
modeStr := c.QueryParam("mode")
|
|
|
|
log.Debug("FileSystem Chmod: sessionId=" + sessionId + " filename=" + filename + " mode=" + modeStr)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
mode, err := strconv.ParseUint(modeStr, 10, 32)
|
|
if err != nil {
|
|
return Fail(c, -1, "invalid mode: "+err.Error())
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
err = sftpClient.Chmod(filename, os.FileMode(mode))
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to chmod: "+err.Error())
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "chmod", filename+" ("+modeStr+")")
|
|
|
|
return Success(c, nil)
|
|
}
|
|
|
|
func (api FileSystemApi) UploadProgressEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
id := c.QueryParam("id")
|
|
|
|
log.Debug("FileSystem UploadProgress: sessionId=" + sessionId + " id=" + id)
|
|
|
|
return Success(c, maps.Map{
|
|
"total": 0,
|
|
"written": 0,
|
|
"percent": 100,
|
|
"speed": 0,
|
|
"elapsedTime": 0,
|
|
"isCompleted": true,
|
|
})
|
|
}
|
|
|
|
func (api FileSystemApi) DownloadEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
filename := c.QueryParam("filename")
|
|
|
|
log.Debug("FileSystem Download: sessionId=" + sessionId + " filename=" + filename)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
f, err := sftpClient.Open(filename)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to open file: "+err.Error())
|
|
}
|
|
defer f.Close()
|
|
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to stat file: "+err.Error())
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "download", filename)
|
|
|
|
c.Response().Header().Set("Content-Disposition", "attachment; filename="+path.Base(filename))
|
|
c.Response().Header().Set("Content-Type", "application/octet-stream")
|
|
c.Response().Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10))
|
|
|
|
return c.Stream(200, "application/octet-stream", f)
|
|
}
|
|
|
|
func (api FileSystemApi) UploadEndpoint(c echo.Context) error {
|
|
sessionId := c.Param("id")
|
|
destDir := c.QueryParam("dir")
|
|
|
|
log.Debug("FileSystem Upload: sessionId=" + sessionId + " dir=" + destDir)
|
|
|
|
sess := session.GlobalSessionManager.GetById(sessionId)
|
|
if sess == nil || sess.NextTerminal == nil || sess.NextTerminal.SshClient == nil {
|
|
return Fail(c, -1, "session not found or SSH connection closed")
|
|
}
|
|
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to get upload file: "+err.Error())
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to open upload file: "+err.Error())
|
|
}
|
|
defer src.Close()
|
|
|
|
sftpClient, err := sftp.NewClient(sess.NextTerminal.SshClient)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create SFTP client: "+err.Error())
|
|
}
|
|
defer sftpClient.Close()
|
|
|
|
destPath := path.Join(destDir, file.Filename)
|
|
dst, err := sftpClient.Create(destPath)
|
|
if err != nil {
|
|
return Fail(c, -1, "failed to create remote file: "+err.Error())
|
|
}
|
|
defer dst.Close()
|
|
|
|
buf := make([]byte, 32*1024)
|
|
for {
|
|
n, err := src.Read(buf)
|
|
if n > 0 {
|
|
_, writeErr := dst.Write(buf[:n])
|
|
if writeErr != nil {
|
|
return Fail(c, -1, "failed to write remote file: "+writeErr.Error())
|
|
}
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
go api.recordFilesystemLog(context.TODO(), sessionId, "upload", destPath)
|
|
|
|
return Success(c, maps.Map{
|
|
"path": destPath,
|
|
"size": file.Size,
|
|
"timestamp": time.Now().UnixMilli(),
|
|
})
|
|
}
|