listdirchildrenlogic.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. // udeServer: 本地目录懒加载 —— 浏览 udeServer 所在 Linux 主机的文件系统。
  2. // 只列目录、不列文件;从根目录 "/" 开始,前端每展开一个节点请求一次该层子目录。
  3. // 安全边界:
  4. // 1. 路径归一化: 必须是绝对路径,filepath.Clean 防穿越
  5. // 2. 黑名单: /proc /sys /dev /run /boot 等系统目录不展示
  6. // 3. 挂载根目录(Mount.BaseDir, 如 /var/ude/mnt)不展示,避免把已挂载点当普通目录选
  7. // 4. 符号链接: DirEntry.IsDir() 对 symlink 返回 false,天然跳过,不跟随
  8. // 5. 权限不足目录: 返回空列表而非报错,树展开不中断
  9. package logic
  10. import (
  11. "context"
  12. "encoding/json"
  13. "fmt"
  14. "io"
  15. "net/http"
  16. "net/url"
  17. "path"
  18. "strings"
  19. "udeServer/internal/apierr"
  20. "udeServer/internal/audit"
  21. "udeServer/internal/svc"
  22. "udeServer/internal/types"
  23. "github.com/zeromicro/go-zero/core/logx"
  24. )
  25. // 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤)
  26. var dirBlacklist = map[string]bool{
  27. "proc": true,
  28. "sys": true,
  29. "dev": true,
  30. "run": true,
  31. "boot": true,
  32. "lost+found": true,
  33. "snap": true,
  34. }
  35. type ListDirChildrenLogic struct {
  36. logx.Logger
  37. ctx context.Context
  38. svcCtx *svc.ServiceContext
  39. }
  40. func NewListDirChildrenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListDirChildrenLogic {
  41. return &ListDirChildrenLogic{
  42. Logger: logx.WithContext(ctx),
  43. ctx: ctx,
  44. svcCtx: svcCtx,
  45. }
  46. }
  47. // --- 远程 /list 响应结构 -------------------------------------------------
  48. type remoteListResp struct {
  49. Path string `json:"path"`
  50. Total int `json:"total"`
  51. Entries []remoteEntry `json:"entries"`
  52. }
  53. type remoteEntry struct {
  54. Name string `json:"name"`
  55. Dir bool `json:"dir"`
  56. Size int64 `json:"size"`
  57. }
  58. func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*types.DirChildrenResp, error) {
  59. ip, port := req.Ip, req.Port
  60. if ip == "" || port == "" {
  61. return nil, fmt.Errorf("外挂服务 IP/端口为空,无法下发挂载")
  62. }
  63. // 1. 归一化: 空 → 根目录; 必须以 / 开头; Clean 防 ../ 穿越
  64. p := strings.TrimSpace(req.Path)
  65. if p == "" {
  66. p = "/"
  67. }
  68. if !strings.HasPrefix(p, "/") {
  69. return nil, apierr.BadRequest("path 必须是绝对路径(以 / 开头)")
  70. }
  71. // path.Clean 而非 filepath.Clean: 该路径发给远程 Linux 外挂程序,
  72. // 必须保持 / 分隔符;filepath 在 Windows 上跑 udeServer 时会把 / 转成 \
  73. dir := path.Clean(p)
  74. return l.notifyListDirChildren(ip, port, dir, req.Iden)
  75. }
  76. // notifyFuseMount 落库成功后回调外挂程序(svc.Ip:svc.Port)的 /api/fuseMount 接口,
  77. // 由外挂程序对保护目录执行 fuse 挂载。一个目录一次调用,请求体:
  78. //
  79. // POST http://{ip}:{port}/api/fuseMount
  80. // { "backing": "<保护目录>-cipher", "mount": "<保护目录>", "ikey": "<密钥内容>" }
  81. //
  82. // ikey 取自 t_key.content(按服务配置的密钥标识 keyIden 查询)。
  83. func (l *ListDirChildrenLogic) notifyListDirChildren(ip, port, dirs, iden string) (*types.DirChildrenResp, error) {
  84. if ip == "" || port == "" {
  85. return nil, fmt.Errorf("外挂服务 IP/端口为空,无法下发挂载")
  86. }
  87. if len(dirs) == 0 {
  88. return nil, fmt.Errorf("没有配置保护目录,无需通知挂载")
  89. }
  90. baseURL := fmt.Sprintf("http://%s:%s/path/lists", ip, port)
  91. // 规范化路径(path.Clean 保持 / 分隔符,与 Linux 外挂程序对齐)
  92. cleanPath := path.Clean(dirs)
  93. // 构造带参数的 GET 请求 URL
  94. reqURL, err := url.Parse(baseURL)
  95. if err != nil {
  96. return nil, fmt.Errorf("解析 URL 失败: %w", err)
  97. }
  98. // 核心修改:往 URL 中添加 ?path=xxx
  99. q := reqURL.Query()
  100. q.Set("path", cleanPath)
  101. reqURL.RawQuery = q.Encode() // 自动处理 URL 编码,生成如 ?path=%2Fdata%2Ftest
  102. // 发起 GET 请求(DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求)
  103. finalURL := reqURL.String()
  104. l.Infof("URL,方便排查参数/编码问题list dirs forward: GET %s", finalURL) // 打出转发 URL,方便排查参数/编码问题
  105. reqDesc := audit.BuildRemoteGetRequest(http.MethodGet, finalURL)
  106. result, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "ListDirChildren", finalURL, reqDesc,
  107. func() (*types.DirChildrenResp, int64, error) {
  108. // doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret);外挂 /path/lists 已免鉴权,带上无害,统一收口避免遗漏
  109. resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
  110. if rerr != nil {
  111. return nil, 0, fmt.Errorf("请求 %s 失败: %w", baseURL, rerr)
  112. }
  113. defer resp.Body.Close() // 确保无论如何都能释放连接
  114. status := int64(resp.StatusCode)
  115. // 检查 HTTP 状态码
  116. if resp.StatusCode != http.StatusOK {
  117. // 把外挂程序返回的错误内容带出来(前 256 字节),便于定位 400 的具体原因
  118. snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
  119. return nil, status, fmt.Errorf("获取目录失败, HTTP 状态码: %d, 外挂程序返回: %s", resp.StatusCode, string(snippet))
  120. }
  121. // 解析 JSON 响应
  122. var parsed *types.DirChildrenResp
  123. if perr := json.NewDecoder(resp.Body).Decode(&parsed); perr != nil {
  124. return nil, status, fmt.Errorf("解析JSON响应失败: %w", perr)
  125. }
  126. return parsed, status, nil
  127. })
  128. if callErr != nil {
  129. return nil, callErr
  130. }
  131. l.Infof("fuse 请求成功, path: %s", cleanPath)
  132. return result, nil
  133. }