mirror of
https://github.com/veops/oneterm.git
synced 2025-11-01 11:22:33 +08:00
perf(backend): file api for ls and download
This commit is contained in:
@@ -5,7 +5,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -13,13 +17,43 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/veops/oneterm/internal/acl"
|
"github.com/veops/oneterm/internal/acl"
|
||||||
|
"github.com/veops/oneterm/internal/guacd"
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/internal/service"
|
"github.com/veops/oneterm/internal/service"
|
||||||
gsession "github.com/veops/oneterm/internal/session"
|
gsession "github.com/veops/oneterm/internal/session"
|
||||||
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
"github.com/veops/oneterm/pkg/errors"
|
"github.com/veops/oneterm/pkg/errors"
|
||||||
"github.com/veops/oneterm/pkg/logger"
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isPermissionError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
errStr := strings.ToLower(err.Error())
|
||||||
|
permissionKeywords := []string{
|
||||||
|
"permission denied",
|
||||||
|
"access denied",
|
||||||
|
"unauthorized",
|
||||||
|
"forbidden",
|
||||||
|
"not authorized",
|
||||||
|
"insufficient privileges",
|
||||||
|
"operation not permitted",
|
||||||
|
"sftp: permission denied",
|
||||||
|
"ssh: permission denied",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, keyword := range permissionKeywords {
|
||||||
|
if strings.Contains(errStr, keyword) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileHistory godoc
|
// GetFileHistory godoc
|
||||||
//
|
//
|
||||||
// @Tags file
|
// @Tags file
|
||||||
@@ -31,13 +65,13 @@ import (
|
|||||||
// @Param end query string false "end, RFC3339"
|
// @Param end query string false "end, RFC3339"
|
||||||
// @Param uid query int false "uid"
|
// @Param uid query int false "uid"
|
||||||
// @Param asset_id query int false "asset id"
|
// @Param asset_id query int false "asset id"
|
||||||
// @Param accout_id query int false "account id"
|
// @Param account_id query int false "account id"
|
||||||
// @Param client_ip query string false "client_ip"
|
// @Param client_ip query string false "client_ip"
|
||||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Session}}
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Session}}
|
||||||
// @Router /file/history [get]
|
// @Router /file/history [get]
|
||||||
func (c *Controller) GetFileHistory(ctx *gin.Context) {
|
func (c *Controller) GetFileHistory(ctx *gin.Context) {
|
||||||
// Create filter conditions
|
// Create filter conditions
|
||||||
filters := make(map[string]interface{})
|
filters := make(map[string]any)
|
||||||
|
|
||||||
// Get user permissions
|
// Get user permissions
|
||||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||||
@@ -112,8 +146,9 @@ func (c *Controller) GetFileHistory(ctx *gin.Context) {
|
|||||||
// @Param asset_id path int true "asset_id"
|
// @Param asset_id path int true "asset_id"
|
||||||
// @Param account_id path int true "account_id"
|
// @Param account_id path int true "account_id"
|
||||||
// @Param dir query string true "dir"
|
// @Param dir query string true "dir"
|
||||||
|
// @Param show_hidden query bool false "show hidden files (default: false)"
|
||||||
// @Success 200 {object} HttpResponse
|
// @Success 200 {object} HttpResponse
|
||||||
// @Router /file/ls/:asset_id/:account_id [post]
|
// @Router /file/ls/:asset_id/:account_id [GET]
|
||||||
func (c *Controller) FileLS(ctx *gin.Context) {
|
func (c *Controller) FileLS(ctx *gin.Context) {
|
||||||
sess := &gsession.Session{
|
sess := &gsession.Session{
|
||||||
Session: &model.Session{
|
Session: &model.Session{
|
||||||
@@ -133,27 +168,52 @@ func (c *Controller) FileLS(ctx *gin.Context) {
|
|||||||
// Use global file service
|
// Use global file service
|
||||||
info, err := service.DefaultFileService.ReadDir(ctx, sess.Session.AssetId, sess.Session.AccountId, ctx.Query("dir"))
|
info, err := service.DefaultFileService.ReadDir(ctx, sess.Session.AssetId, sess.Session.AccountId, ctx.Query("dir"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isPermissionError(err) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, fmt.Errorf("permission denied"))
|
||||||
|
} else {
|
||||||
ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter hidden files unless show_hidden is true
|
||||||
|
showHidden := cast.ToBool(ctx.Query("show_hidden"))
|
||||||
|
if !showHidden {
|
||||||
|
info = lo.Filter(info, func(f fs.FileInfo, _ int) bool {
|
||||||
|
return !strings.HasPrefix(f.Name(), ".")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
res := &ListData{
|
res := &ListData{
|
||||||
Count: int64(len(info)),
|
Count: int64(len(info)),
|
||||||
List: lo.Map(info, func(f fs.FileInfo, _ int) any {
|
List: lo.Map(info, func(f fs.FileInfo, _ int) any {
|
||||||
|
var target string
|
||||||
|
if f.Mode()&os.ModeSymlink != 0 {
|
||||||
|
cli, err := service.GetFileManager().GetFileClient(sess.Session.AssetId, sess.Session.AccountId)
|
||||||
|
if err == nil {
|
||||||
|
linkPath := filepath.Join(ctx.Query("dir"), f.Name())
|
||||||
|
if linkTarget, err := cli.ReadLink(linkPath); err == nil {
|
||||||
|
target = linkTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return &service.FileInfo{
|
return &service.FileInfo{
|
||||||
Name: f.Name(),
|
Name: f.Name(),
|
||||||
IsDir: f.IsDir(),
|
IsDir: f.IsDir(),
|
||||||
Size: f.Size(),
|
Size: f.Size(),
|
||||||
Mode: f.Mode().String(),
|
Mode: f.Mode().String(),
|
||||||
|
IsLink: f.Mode()&os.ModeSymlink != 0,
|
||||||
|
Target: target,
|
||||||
|
ModTime: f.ModTime().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(res))
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileMkdir file
|
// FileMkdir godoc
|
||||||
//
|
//
|
||||||
// @Tags account
|
// @Tags file
|
||||||
// @Param asset_id path int true "asset_id"
|
// @Param asset_id path int true "asset_id"
|
||||||
// @Param account_id path int true "account_id"
|
// @Param account_id path int true "account_id"
|
||||||
// @Param dir query string true "dir "
|
// @Param dir query string true "dir "
|
||||||
@@ -276,8 +336,7 @@ func (c *Controller) FileUpload(ctx *gin.Context) {
|
|||||||
// @Param asset_id path int true "asset_id"
|
// @Param asset_id path int true "asset_id"
|
||||||
// @Param account_id path int true "account_id"
|
// @Param account_id path int true "account_id"
|
||||||
// @Param dir query string true "dir"
|
// @Param dir query string true "dir"
|
||||||
// @Param filename query string true "filename"
|
// @Param names query string true "names (comma-separated for multiple files)"
|
||||||
// @Param file formData string true "file field name"
|
|
||||||
// @Success 200 {object} HttpResponse
|
// @Success 200 {object} HttpResponse
|
||||||
// @Router /file/download/:asset_id/:account_id [get]
|
// @Router /file/download/:asset_id/:account_id [get]
|
||||||
func (c *Controller) FileDownload(ctx *gin.Context) {
|
func (c *Controller) FileDownload(ctx *gin.Context) {
|
||||||
@@ -298,22 +357,38 @@ func (c *Controller) FileDownload(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use global file service
|
filenameParam := ctx.Query("names")
|
||||||
rf, err := service.DefaultFileService.Open(ctx, sess.Session.AssetId, sess.Session.AccountId, filepath.Join(ctx.Query("dir"), ctx.Query("filename")))
|
if filenameParam == "" {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": "names parameter is required"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filenames := lo.Filter(
|
||||||
|
lo.Map(strings.Split(filenameParam, ","), func(name string, _ int) string {
|
||||||
|
return strings.TrimSpace(name)
|
||||||
|
}),
|
||||||
|
func(name string, _ int) bool {
|
||||||
|
return name != ""
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(filenames) == 0 {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": "no valid filenames provided"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, downloadFilename, fileSize, err := service.DefaultFileService.DownloadMultiple(ctx, sess.Session.AssetId, sess.Session.AccountId, ctx.Query("dir"), filenames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isPermissionError(err) {
|
||||||
|
ctx.AbortWithError(http.StatusForbidden, &errors.ApiError{Code: errors.ErrNoPerm, Data: map[string]any{"err": err}})
|
||||||
|
} else {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
defer rf.Close()
|
// Record file operation history
|
||||||
|
|
||||||
content, err := io.ReadAll(rf)
|
|
||||||
if err != nil {
|
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &errors.ApiError{Code: errors.ErrInternal, Data: map[string]any{"err": err}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create history record
|
|
||||||
h := &model.FileHistory{
|
h := &model.FileHistory{
|
||||||
Uid: currentUser.GetUid(),
|
Uid: currentUser.GetUid(),
|
||||||
UserName: currentUser.GetUserName(),
|
UserName: currentUser.GetUserName(),
|
||||||
@@ -322,16 +397,421 @@ func (c *Controller) FileDownload(ctx *gin.Context) {
|
|||||||
ClientIp: ctx.ClientIP(),
|
ClientIp: ctx.ClientIP(),
|
||||||
Action: model.FILE_ACTION_DOWNLOAD,
|
Action: model.FILE_ACTION_DOWNLOAD,
|
||||||
Dir: ctx.Query("dir"),
|
Dir: ctx.Query("dir"),
|
||||||
Filename: ctx.Query("filename"),
|
Filename: strings.Join(filenames, ","),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
service.DefaultFileService.AddFileHistory(ctx, h)
|
||||||
|
|
||||||
|
// Set response headers for file download
|
||||||
|
ctx.Header("Content-Type", "application/octet-stream")
|
||||||
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", downloadFilename))
|
||||||
|
ctx.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
ctx.Header("Pragma", "no-cache")
|
||||||
|
ctx.Header("Expires", "0")
|
||||||
|
|
||||||
|
// Set content length if known
|
||||||
|
if fileSize > 0 {
|
||||||
|
ctx.Header("Content-Length", fmt.Sprintf("%d", fileSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = service.DefaultFileService.AddFileHistory(ctx, h); err != nil {
|
// Stream file content directly to response
|
||||||
logger.L().Error("record download failed", zap.Error(err), zap.Any("history", h))
|
ctx.Status(http.StatusOK)
|
||||||
|
|
||||||
|
// Use streaming copy with buffer to handle large files efficiently
|
||||||
|
buffer := make([]byte, 32*1024) // 32KB buffer for optimal performance
|
||||||
|
_, err = io.CopyBuffer(ctx.Writer, reader, buffer)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("File transfer failed", zap.Error(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rw := ctx.Writer
|
// RDP File Transfer Methods
|
||||||
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", ctx.Query("filename")))
|
|
||||||
rw.Header().Set("Content-Type", "application/octet-stream")
|
// RDPFileInfo represents file information for RDP sessions
|
||||||
rw.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
|
type RDPFileInfo struct {
|
||||||
rw.Write(content)
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
IsDir bool `json:"is_dir"`
|
||||||
|
ModTime string `json:"mod_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPMkdirRequest represents directory creation request for RDP
|
||||||
|
type RDPMkdirRequest struct {
|
||||||
|
Path string `json:"path" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPFileList lists files in RDP session drive
|
||||||
|
// @Summary List RDP session files
|
||||||
|
// @Description Get file list for RDP session drive
|
||||||
|
// @Tags RDP File
|
||||||
|
// @Param session_id path string true "Session ID"
|
||||||
|
// @Param path query string false "Directory path"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /api/v1/rdp/sessions/{session_id}/files [get]
|
||||||
|
func (c *Controller) RDPFileList(ctx *gin.Context) {
|
||||||
|
sessionId := ctx.Param("session_id")
|
||||||
|
path := ctx.DefaultQuery("path", "/")
|
||||||
|
|
||||||
|
// Check if session exists and user has permission
|
||||||
|
if !c.hasRDPSessionPermission(ctx, sessionId) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No permission to access this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session tunnel
|
||||||
|
tunnel := c.getRDPSessionTunnel(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
ctx.JSON(http.StatusNotFound, HttpResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Message: "Session not found or not active",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if RDP drive is enabled
|
||||||
|
if !c.isRDPDriveEnabled(tunnel) {
|
||||||
|
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: "RDP drive is not enabled for this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send file list request through Guacamole protocol
|
||||||
|
files, err := c.requestRDPFileList(tunnel, path)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to get RDP file list", zap.Error(err))
|
||||||
|
ctx.JSON(http.StatusInternalServerError, HttpResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Message: "Failed to get file list",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "ok",
|
||||||
|
Data: files,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPFileUpload uploads file to RDP session drive
|
||||||
|
// @Summary Upload file to RDP session
|
||||||
|
// @Description Upload file to RDP session drive
|
||||||
|
// @Tags RDP File
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param session_id path string true "Session ID"
|
||||||
|
// @Param file formData file true "File to upload"
|
||||||
|
// @Param path formData string false "Target directory path"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /api/v1/rdp/sessions/{session_id}/files/upload [post]
|
||||||
|
func (c *Controller) RDPFileUpload(ctx *gin.Context) {
|
||||||
|
sessionId := ctx.Param("session_id")
|
||||||
|
targetPath := ctx.DefaultPostForm("path", "/")
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if !c.hasRDPSessionPermission(ctx, sessionId) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No permission to access this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get uploaded file
|
||||||
|
file, header, err := ctx.Request.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: "Failed to get uploaded file",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Get session tunnel
|
||||||
|
tunnel := c.getRDPSessionTunnel(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
ctx.JSON(http.StatusNotFound, HttpResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Message: "Session not found or not active",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is allowed
|
||||||
|
if !c.isRDPUploadAllowed(tunnel) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "File upload is disabled for this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, HttpResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Message: "Failed to read file content",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send upload request through Guacamole protocol
|
||||||
|
fullPath := filepath.Join(targetPath, header.Filename)
|
||||||
|
err = c.uploadRDPFile(tunnel, fullPath, content)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to upload file to RDP session", zap.Error(err))
|
||||||
|
ctx.JSON(http.StatusInternalServerError, HttpResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Message: "Failed to upload file",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record file operation history
|
||||||
|
c.recordRDPFileHistory(ctx, sessionId, "upload", fullPath, int64(len(content)))
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "ok",
|
||||||
|
Data: gin.H{
|
||||||
|
"message": "File uploaded successfully",
|
||||||
|
"path": fullPath,
|
||||||
|
"size": len(content),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPFileDownload downloads file from RDP session drive
|
||||||
|
// @Summary Download file from RDP session
|
||||||
|
// @Description Download file from RDP session drive
|
||||||
|
// @Tags RDP File
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/octet-stream
|
||||||
|
// @Param session_id path string true "Session ID"
|
||||||
|
// @Param path query string true "File path"
|
||||||
|
// @Success 200 {file} binary
|
||||||
|
// @Router /api/v1/rdp/sessions/{session_id}/files/download [get]
|
||||||
|
func (c *Controller) RDPFileDownload(ctx *gin.Context) {
|
||||||
|
sessionId := ctx.Param("session_id")
|
||||||
|
filePath := ctx.Query("path")
|
||||||
|
|
||||||
|
if filePath == "" {
|
||||||
|
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: "File path is required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if !c.hasRDPSessionPermission(ctx, sessionId) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No permission to access this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session tunnel
|
||||||
|
tunnel := c.getRDPSessionTunnel(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
ctx.JSON(http.StatusNotFound, HttpResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Message: "Session not found or not active",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if download is allowed
|
||||||
|
if !c.isRDPDownloadAllowed(tunnel) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "File download is disabled for this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request file download through Guacamole protocol
|
||||||
|
content, err := c.downloadRDPFile(tunnel, filePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to download file from RDP session", zap.Error(err))
|
||||||
|
ctx.JSON(http.StatusInternalServerError, HttpResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Message: "Failed to download file",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record file operation history
|
||||||
|
c.recordRDPFileHistory(ctx, sessionId, "download", filePath, int64(len(content)))
|
||||||
|
|
||||||
|
// Set response headers
|
||||||
|
filename := filepath.Base(filePath)
|
||||||
|
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
|
ctx.Header("Content-Type", "application/octet-stream")
|
||||||
|
ctx.Header("Content-Length", strconv.Itoa(len(content)))
|
||||||
|
|
||||||
|
ctx.Data(http.StatusOK, "application/octet-stream", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPFileMkdir creates directory in RDP session drive
|
||||||
|
// @Summary Create directory in RDP session
|
||||||
|
// @Description Create directory in RDP session drive
|
||||||
|
// @Tags RDP File
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param session_id path string true "Session ID"
|
||||||
|
// @Param request body RDPMkdirRequest true "Directory creation request"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /api/v1/rdp/sessions/{session_id}/files/mkdir [post]
|
||||||
|
func (c *Controller) RDPFileMkdir(ctx *gin.Context) {
|
||||||
|
sessionId := ctx.Param("session_id")
|
||||||
|
|
||||||
|
var req RDPMkdirRequest
|
||||||
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||||
|
ctx.JSON(http.StatusBadRequest, HttpResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Message: "Invalid request parameters",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if !c.hasRDPSessionPermission(ctx, sessionId) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "No permission to access this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session tunnel
|
||||||
|
tunnel := c.getRDPSessionTunnel(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
ctx.JSON(http.StatusNotFound, HttpResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Message: "Session not found or not active",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if upload is allowed (mkdir is considered an upload operation)
|
||||||
|
if !c.isRDPUploadAllowed(tunnel) {
|
||||||
|
ctx.JSON(http.StatusForbidden, HttpResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "Directory creation is disabled for this session",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send mkdir request through Guacamole protocol
|
||||||
|
err := c.createRDPDirectory(tunnel, req.Path)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Error("Failed to create directory in RDP session", zap.Error(err))
|
||||||
|
ctx.JSON(http.StatusInternalServerError, HttpResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Message: "Failed to create directory",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record file operation history
|
||||||
|
c.recordRDPFileHistory(ctx, sessionId, "mkdir", req.Path, 0)
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, HttpResponse{
|
||||||
|
Code: 0,
|
||||||
|
Message: "ok",
|
||||||
|
Data: gin.H{
|
||||||
|
"message": "Directory created successfully",
|
||||||
|
"path": req.Path,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDP Helper methods
|
||||||
|
|
||||||
|
func (c *Controller) hasRDPSessionPermission(ctx *gin.Context, sessionId string) bool {
|
||||||
|
// TODO: Implement proper session permission check
|
||||||
|
// This should verify that the current user has access to the specified session
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getRDPSessionTunnel(sessionId string) *guacd.Tunnel {
|
||||||
|
// TODO: Implement session tunnel retrieval
|
||||||
|
// This should get the active Guacamole tunnel for the session
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) isRDPDriveEnabled(tunnel *guacd.Tunnel) bool {
|
||||||
|
// Check if RDP drive is enabled in tunnel configuration
|
||||||
|
return tunnel.Config.Parameters["enable-drive"] == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) isRDPUploadAllowed(tunnel *guacd.Tunnel) bool {
|
||||||
|
return tunnel.Config.Parameters["disable-upload"] != "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) isRDPDownloadAllowed(tunnel *guacd.Tunnel) bool {
|
||||||
|
return tunnel.Config.Parameters["disable-download"] != "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) requestRDPFileList(tunnel *guacd.Tunnel, path string) ([]RDPFileInfo, error) {
|
||||||
|
// TODO: Implement Guacamole protocol communication for file listing
|
||||||
|
// This would involve sending appropriate Guacamole instructions and parsing responses
|
||||||
|
return nil, fmt.Errorf("not implemented: RDP file listing through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) uploadRDPFile(tunnel *guacd.Tunnel, path string, content []byte) error {
|
||||||
|
// TODO: Implement Guacamole protocol communication for file upload
|
||||||
|
// This would involve sending file-upload instruction with base64 encoded content
|
||||||
|
return fmt.Errorf("not implemented: RDP file upload through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) downloadRDPFile(tunnel *guacd.Tunnel, path string) ([]byte, error) {
|
||||||
|
// TODO: Implement Guacamole protocol communication for file download
|
||||||
|
// This would involve sending file-download instruction and receiving file data
|
||||||
|
return nil, fmt.Errorf("not implemented: RDP file download through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) createRDPDirectory(tunnel *guacd.Tunnel, path string) error {
|
||||||
|
// TODO: Implement Guacamole protocol communication for directory creation
|
||||||
|
return fmt.Errorf("not implemented: RDP directory creation through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) recordRDPFileHistory(ctx *gin.Context, sessionId, operation, path string, size int64) {
|
||||||
|
// Record file operation in history
|
||||||
|
history := &model.FileHistory{
|
||||||
|
Uid: 0, // TODO: Get current user ID
|
||||||
|
UserName: "", // TODO: Get current user name
|
||||||
|
AssetId: 0, // TODO: Get asset ID from session
|
||||||
|
AccountId: 0, // TODO: Get account ID from session
|
||||||
|
ClientIp: ctx.ClientIP(),
|
||||||
|
Action: c.getRDPActionCode(operation),
|
||||||
|
Dir: filepath.Dir(path),
|
||||||
|
Filename: filepath.Base(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
fileService := service.NewFileService(repository.NewFileRepository(dbpkg.DB))
|
||||||
|
if err := fileService.AddFileHistory(ctx, history); err != nil {
|
||||||
|
logger.L().Error("Failed to record RDP file history", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) getRDPActionCode(operation string) int {
|
||||||
|
switch operation {
|
||||||
|
case "upload":
|
||||||
|
return model.FILE_ACTION_UPLOAD
|
||||||
|
case "download":
|
||||||
|
return model.FILE_ACTION_DOWNLOAD
|
||||||
|
case "mkdir":
|
||||||
|
return model.FILE_ACTION_MKDIR
|
||||||
|
default:
|
||||||
|
return model.FILE_ACTION_LS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
|
|
||||||
"github.com/veops/oneterm/internal/model"
|
"github.com/veops/oneterm/internal/model"
|
||||||
"github.com/veops/oneterm/internal/repository"
|
"github.com/veops/oneterm/internal/repository"
|
||||||
"github.com/veops/oneterm/internal/tunneling"
|
"github.com/veops/oneterm/internal/tunneling"
|
||||||
dbpkg "github.com/veops/oneterm/pkg/db"
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -65,6 +69,9 @@ type FileInfo struct {
|
|||||||
IsDir bool `json:"is_dir"`
|
IsDir bool `json:"is_dir"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
|
IsLink bool `json:"is_link"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
ModTime string `json:"mod_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFileManager() *FileManager {
|
func GetFileManager() *FileManager {
|
||||||
@@ -122,8 +129,16 @@ type IFileService interface {
|
|||||||
MkdirAll(ctx context.Context, assetId, accountId int, dir string) error
|
MkdirAll(ctx context.Context, assetId, accountId int, dir string) error
|
||||||
Create(ctx context.Context, assetId, accountId int, path string) (io.WriteCloser, error)
|
Create(ctx context.Context, assetId, accountId int, path string) (io.WriteCloser, error)
|
||||||
Open(ctx context.Context, assetId, accountId int, path string) (io.ReadCloser, error)
|
Open(ctx context.Context, assetId, accountId int, path string) (io.ReadCloser, error)
|
||||||
|
Stat(ctx context.Context, assetId, accountId int, path string) (fs.FileInfo, error)
|
||||||
|
DownloadMultiple(ctx context.Context, assetId, accountId int, dir string, filenames []string) (io.ReadCloser, string, int64, error)
|
||||||
AddFileHistory(ctx context.Context, history *model.FileHistory) error
|
AddFileHistory(ctx context.Context, history *model.FileHistory) error
|
||||||
GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error)
|
GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error)
|
||||||
|
|
||||||
|
// RDP file transfer methods
|
||||||
|
RDPReadDir(ctx context.Context, sessionId, dir string) ([]fs.FileInfo, error)
|
||||||
|
RDPMkdirAll(ctx context.Context, sessionId, dir string) error
|
||||||
|
RDPUploadFile(ctx context.Context, sessionId, filename string, content []byte) error
|
||||||
|
RDPDownloadFile(ctx context.Context, sessionId, filename string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// File service implementation
|
// File service implementation
|
||||||
@@ -161,6 +176,7 @@ func (s *FileService) Create(ctx context.Context, assetId, accountId int, path s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.Create(path)
|
return cli.Create(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +186,159 @@ func (s *FileService) Open(ctx context.Context, assetId, accountId int, path str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.Open(path)
|
return cli.Open(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stat gets file/directory information
|
||||||
|
func (s *FileService) Stat(ctx context.Context, assetId, accountId int, path string) (fs.FileInfo, error) {
|
||||||
|
cli, err := GetFileManager().GetFileClient(assetId, accountId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.Stat(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadMultiple handles downloading single file or multiple files/directories as ZIP
|
||||||
|
func (s *FileService) DownloadMultiple(ctx context.Context, assetId, accountId int, dir string, filenames []string) (io.ReadCloser, string, int64, error) {
|
||||||
|
cli, err := GetFileManager().GetFileClient(assetId, accountId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize all filenames for security
|
||||||
|
var sanitizedFilenames []string
|
||||||
|
for _, filename := range filenames {
|
||||||
|
sanitized, err := sanitizeFilename(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", 0, fmt.Errorf("invalid filename '%s': %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizedFilenames = append(sanitizedFilenames, sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one file, check if it's a regular file first
|
||||||
|
if len(sanitizedFilenames) == 1 {
|
||||||
|
fullPath := filepath.Join(dir, sanitizedFilenames[0])
|
||||||
|
fileInfo, err := cli.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a regular file, return directly
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
reader, err := cli.Open(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", 0, err
|
||||||
|
}
|
||||||
|
return reader, sanitizedFilenames[0], fileInfo.Size(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple files or contains directory, create ZIP
|
||||||
|
return s.createZipArchive(cli, dir, sanitizedFilenames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createZipArchive creates a ZIP archive containing the specified files/directories
|
||||||
|
func (s *FileService) createZipArchive(cli *sftp.Client, baseDir string, filenames []string) (io.ReadCloser, string, int64, error) {
|
||||||
|
// Generate ZIP filename
|
||||||
|
var zipName string
|
||||||
|
if len(filenames) == 1 {
|
||||||
|
zipName = filenames[0] + ".zip"
|
||||||
|
} else {
|
||||||
|
zipName = "download.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use pipe for true streaming without memory buffering
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
|
||||||
|
// Create ZIP in a separate goroutine
|
||||||
|
go func() {
|
||||||
|
defer pipeWriter.Close()
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(pipeWriter)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
// Add each file/directory to ZIP
|
||||||
|
for _, filename := range filenames {
|
||||||
|
fullPath := filepath.Join(baseDir, filename)
|
||||||
|
|
||||||
|
if err := s.addToZip(cli, zipWriter, baseDir, filename, fullPath); err != nil {
|
||||||
|
logger.L().Error("Failed to add file to ZIP", zap.String("path", fullPath), zap.Error(err))
|
||||||
|
pipeWriter.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Return pipe reader for streaming, size unknown (-1)
|
||||||
|
return pipeReader, zipName, -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToZip recursively adds files/directories to ZIP archive
|
||||||
|
func (s *FileService) addToZip(cli *sftp.Client, zipWriter *zip.Writer, baseDir, relativePath, fullPath string) error {
|
||||||
|
fileInfo, err := cli.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
// Add directory
|
||||||
|
return s.addDirToZip(cli, zipWriter, fullPath, relativePath)
|
||||||
|
} else {
|
||||||
|
// Add file
|
||||||
|
return s.addFileToZip(cli, zipWriter, fullPath, relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addFileToZip adds a single file to ZIP archive
|
||||||
|
func (s *FileService) addFileToZip(cli *sftp.Client, zipWriter *zip.Writer, fullPath, relativePath string) error {
|
||||||
|
// Open remote file
|
||||||
|
file, err := cli.Open(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Create file in ZIP
|
||||||
|
zipFile, err := zipWriter.Create(relativePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy file content
|
||||||
|
_, err = io.Copy(zipFile, file)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDirToZip recursively adds a directory to ZIP archive
|
||||||
|
func (s *FileService) addDirToZip(cli *sftp.Client, zipWriter *zip.Writer, fullPath, relativePath string) error {
|
||||||
|
// Read directory contents
|
||||||
|
entries, err := cli.ReadDir(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If directory is empty, create directory entry
|
||||||
|
if len(entries) == 0 {
|
||||||
|
_, err := zipWriter.Create(relativePath + "/")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively add each entry in the directory
|
||||||
|
for _, entry := range entries {
|
||||||
|
entryFullPath := filepath.Join(fullPath, entry.Name())
|
||||||
|
entryRelativePath := filepath.Join(relativePath, entry.Name())
|
||||||
|
|
||||||
|
if err := s.addToZip(cli, zipWriter, fullPath, entryRelativePath, entryFullPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddFileHistory adds a file history record
|
// AddFileHistory adds a file history record
|
||||||
func (s *FileService) AddFileHistory(ctx context.Context, history *model.FileHistory) error {
|
func (s *FileService) AddFileHistory(ctx context.Context, history *model.FileHistory) error {
|
||||||
return s.repo.AddFileHistory(ctx, history)
|
return s.repo.AddFileHistory(ctx, history)
|
||||||
@@ -182,3 +348,65 @@ func (s *FileService) AddFileHistory(ctx context.Context, history *model.FileHis
|
|||||||
func (s *FileService) GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) {
|
func (s *FileService) GetFileHistory(ctx context.Context, filters map[string]interface{}) ([]*model.FileHistory, int64, error) {
|
||||||
return s.repo.GetFileHistory(ctx, filters)
|
return s.repo.GetFileHistory(ctx, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RDP file transfer methods implementation
|
||||||
|
|
||||||
|
// RDPReadDir reads directory contents for RDP session
|
||||||
|
func (s *FileService) RDPReadDir(ctx context.Context, sessionId, dir string) ([]fs.FileInfo, error) {
|
||||||
|
// Get session tunnel to access file transfer manager
|
||||||
|
tunnel := tunneling.GetTunnelBySessionId(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
return nil, fmt.Errorf("session not found: %s", sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For RDP sessions, we need to check if drive is enabled
|
||||||
|
// This would need to be implemented based on the actual session configuration
|
||||||
|
// For now, return an error indicating RDP file operations need to be handled differently
|
||||||
|
return nil, fmt.Errorf("RDP file operations should be handled through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPMkdirAll creates directory for RDP session
|
||||||
|
func (s *FileService) RDPMkdirAll(ctx context.Context, sessionId, dir string) error {
|
||||||
|
tunnel := tunneling.GetTunnelBySessionId(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
return fmt.Errorf("session not found: %s", sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("RDP file operations should be handled through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPUploadFile uploads file for RDP session
|
||||||
|
func (s *FileService) RDPUploadFile(ctx context.Context, sessionId, filename string, content []byte) error {
|
||||||
|
tunnel := tunneling.GetTunnelBySessionId(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
return fmt.Errorf("session not found: %s", sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("RDP file operations should be handled through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPDownloadFile downloads file for RDP session
|
||||||
|
func (s *FileService) RDPDownloadFile(ctx context.Context, sessionId, filename string) ([]byte, error) {
|
||||||
|
tunnel := tunneling.GetTunnelBySessionId(sessionId)
|
||||||
|
if tunnel == nil {
|
||||||
|
return nil, fmt.Errorf("session not found: %s", sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("RDP file operations should be handled through Guacamole protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeFilename(filename string) (string, error) {
|
||||||
|
// Remove any path traversal attempts
|
||||||
|
if strings.Contains(filename, "..") ||
|
||||||
|
strings.Contains(filename, "/") ||
|
||||||
|
strings.Contains(filename, "\\") {
|
||||||
|
return "", fmt.Errorf("invalid filename: path traversal detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove null bytes and control characters
|
||||||
|
if strings.ContainsAny(filename, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f") {
|
||||||
|
return "", fmt.Errorf("invalid filename: control characters detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user