// udeServer: 本地目录懒加载 —— 浏览 udeServer 所在 Linux 主机的文件系统。 // 只列目录、不列文件;从根目录 "/" 开始,前端每展开一个节点请求一次该层子目录。 // 安全边界: // 1. 路径归一化: 必须是绝对路径,filepath.Clean 防穿越 // 2. 黑名单: /proc /sys /dev /run /boot 等系统目录不展示 // 3. 挂载根目录(Mount.BaseDir, 如 /var/ude/mnt)不展示,避免把已挂载点当普通目录选 // 4. 符号链接: DirEntry.IsDir() 对 symlink 返回 false,天然跳过,不跟随 // 5. 权限不足目录: 返回空列表而非报错,树展开不中断 package logic import ( "context" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "path" "strings" "time" "udeServer/internal/apierr" "udeServer/internal/svc" "udeServer/internal/types" "github.com/zeromicro/go-zero/core/logx" ) // 根层直接屏蔽的系统/虚拟目录(仅在这些目录位于 "/" 下时过滤) var dirBlacklist = map[string]bool{ "proc": true, "sys": true, "dev": true, "run": true, "boot": true, "lost+found": true, "snap": true, } type ListDirChildrenLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewListDirChildrenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListDirChildrenLogic { return &ListDirChildrenLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // --- 远程 /list 响应结构 ------------------------------------------------- type remoteListResp struct { Path string `json:"path"` Total int `json:"total"` Entries []remoteEntry `json:"entries"` } type remoteEntry struct { Name string `json:"name"` Dir bool `json:"dir"` Size int64 `json:"size"` } func (l *ListDirChildrenLogic) ListDirChildren(req *types.ListDirChildrenReq) (*types.DirChildrenResp, error) { // 1. 归一化: 空 → 根目录; 必须以 / 开头; Clean 防 ../ 穿越 p := strings.TrimSpace(req.Path) if p == "" { p = "/" } if !strings.HasPrefix(p, "/") { return nil, apierr.BadRequest("path 必须是绝对路径(以 / 开头)") } // 目标机是 Linux,统一用 path.Clean,避免依赖本机 OS 分隔符 dir := path.Clean(p) if req.Ip == "" { return nil, apierr.BadRequest("ip 不能为空") } if req.Port == "" { return nil, apierr.BadRequest("port 不能为空") } // 2. 调用远程 /list 接口(只读一层,天然懒加载) remote, err := l.fetchRemoteList(req.Ip, req.Port, dir) if err != nil { return nil, err } // 3. 转换成前端节点结构(响应结构与原来保持一致) nodes := make([]types.LocalDirNode, 0, len(remote.Entries)) for _, e := range remote.Entries { if !e.Dir { continue // 只要目录;文件/符号链接(dir:false)一律跳过 } name := e.Name if dir == "/" && dirBlacklist[name] { continue // 根层黑名单系统目录 } full := path.Join(dir, name) nodes = append(nodes, types.LocalDirNode{ Name: name, Path: full, IsDir: true, // 远程 /list 不返回"是否有子目录",保守给 true; // 前端真正展开时会再发一次 /list,行为上仍是懒加载 HasChildren: true, }) } return &types.DirChildrenResp{Nodes: nodes}, nil } // --- 调用远程 /list ------------------------------------------------------ func (l *ListDirChildrenLogic) fetchRemoteList(ip, port, dir string) (*remoteListResp, error) { u := &url.URL{ Scheme: "http", Host: net.JoinHostPort(ip, port), Path: "/list", } q := url.Values{} q.Set("path", dir) u.RawQuery = q.Encode() ctx, cancel := context.WithTimeout(l.ctx, 10*time.Second) defer cancel() httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, apierr.BadRequest(fmt.Sprintf("构造远程请求失败: %v", err)) } httpResp, err := http.DefaultClient.Do(httpReq) if err != nil { l.Errorf("call remote /list %s failed: %v", u.String(), err) return nil, apierr.BadRequest(fmt.Sprintf("调用远程接口失败: %v", err)) } defer httpResp.Body.Close() // 保护性限制:一个目录条目正常远小于 8MB body, readErr := io.ReadAll(io.LimitReader(httpResp.Body, 8<<20)) if readErr != nil { return nil, apierr.BadRequest(fmt.Sprintf("读取远程响应失败: %v", readErr)) } switch httpResp.StatusCode { case http.StatusOK: // ok,继续解析 case http.StatusBadRequest: // 远程 400 自带中文说明,原样透传给前端 return nil, apierr.BadRequest(extractRemoteMessage(body)) case http.StatusMethodNotAllowed: return nil, apierr.BadRequest("远程服务拒绝了 GET 请求") default: return nil, apierr.BadRequest(fmt.Sprintf( "远程接口异常(%d): %s", httpResp.StatusCode, extractRemoteMessage(body))) } var out remoteListResp if err := json.Unmarshal(body, &out); err != nil { return nil, apierr.BadRequest(fmt.Sprintf("解析远程响应失败: %v", err)) } return &out, nil } // 从远程错误响应中提取中文说明(兼容 message/msg/error 字段) func extractRemoteMessage(body []byte) string { if len(body) == 0 { return "远程接口返回空响应" } var wrap struct { Message string `json:"message"` Msg string `json:"msg"` Error string `json:"error"` } if json.Unmarshal(body, &wrap) == nil { switch { case wrap.Message != "": return wrap.Message case wrap.Msg != "": return wrap.Msg case wrap.Error != "": return wrap.Error } } return string(body) } // hasSubDir 判断目录下是否还有子目录(只读一层、找到一个即返回,开销可控) func hasSubDir(dir string) bool { subs, err := os.ReadDir(dir) if err != nil { return false } for _, s := range subs { if s.IsDir() { return true } } return false }