package filesystem import ( "context" "encoding/base64" "fmt" "log" "net/http" "net/url" "os" "path/filepath" "strings" "sync" "time" ) // LocalFileServer 本地文件服务器(独立的 HTTP 服务器) type LocalFileServer struct { server *http.Server addr string mu sync.RWMutex } var ( localFileServer *LocalFileServer localFileServerOnce sync.Once ) // StartLocalFileServer 启动本地文件服务器 func StartLocalFileServer() (string, error) { var initErr error localFileServerOnce.Do(func() { // 创建多路复用器 mux := http.NewServeMux() // 注册 /localfs/ 路由 mux.HandleFunc("/localfs/", handleLocalFileRequest) // 创建服务器(固定端口) server := &http.Server{ Addr: "localhost:18765", Handler: mux, } // 启动服务器 go func() { log.Printf("[LocalFileServer] 正在启动...") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("[LocalFileServer] 启动失败: %v", err) initErr = err } }() localFileServer = &LocalFileServer{ server: server, addr: "localhost:18765", } log.Printf("[LocalFileServer] 已启动,监听: %s", localFileServer.addr) }) if localFileServer == nil { return "", initErr } return localFileServer.addr, initErr } // handleLocalFileRequest 处理本地文件请求 func handleLocalFileRequest(w http.ResponseWriter, r *http.Request) { // CORS 头:允许所有源访问(因为这是本地文件服务器) w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "*") // 处理 OPTIONS 预检请求 if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } // 只处理 GET 请求 if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } log.Printf("[LocalFileHandler] 收到请求: %s", r.URL.Path) // 从 URL 路径获取文件路径(移除 /localfs/ 前缀) pathPart := strings.TrimPrefix(r.URL.Path, "/localfs/") log.Printf("[LocalFileHandler] TrimPrefix 后: %s", pathPart) if pathPart == "" || pathPart == r.URL.Path { log.Printf("[LocalFileHandler] 路径前缀无效") http.Error(w, "Invalid path. Use: /localfs/C:/path/to/file", http.StatusBadRequest) return } // 🔒 修复:先进行URL解码,防止路径遍历攻击 decodedPath, err := url.QueryUnescape(pathPart) if err != nil { log.Printf("[LocalFileHandler] URL解码失败: %v", err) http.Error(w, "Invalid path encoding", http.StatusBadRequest) return } log.Printf("[LocalFileHandler] URL解码后: %s", decodedPath) // 🔒 修复:在路径转换前检查是否包含危险字符 if strings.Contains(decodedPath, "..") { log.Printf("[LocalFileHandler] 检测到路径遍历尝试") http.Error(w, "Path traversal detected", http.StatusForbidden) return } // 路径转换(统一使用反斜杠) filePath := strings.ReplaceAll(decodedPath, "/", "\\") filePath = filepath.Clean(filePath) log.Printf("[LocalFileHandler] 最终路径: %s", filePath) // 安全检查 if !isSafePath(filePath) { log.Printf("[LocalFileHandler] 路径未通过安全检查: %s", filePath) http.Error(w, "Unsafe path", http.StatusForbidden) return } // 🔒 修复:文件类型白名单检查 ext := strings.ToLower(filepath.Ext(filePath)) if !isAllowedFileType(ext) { log.Printf("[LocalFileHandler] 不允许的文件类型: %s", ext) http.Error(w, fmt.Sprintf("Forbidden file type: %s", ext), http.StatusForbidden) return } // 检查文件是否存在 fileInfo, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { log.Printf("[LocalFileHandler] 文件不存在: %s", filePath) http.Error(w, fmt.Sprintf("File not found: %s", filePath), http.StatusNotFound) } else { log.Printf("[LocalFileHandler] 无法访问文件: %v", err) http.Error(w, fmt.Sprintf("Failed to stat file: %v", err), http.StatusInternalServerError) } return } // 🔒 限制文件大小(最大500MB) const maxFileSize = 500 * 1024 * 1024 if fileInfo.Size() > maxFileSize { log.Printf("[LocalFileHandler] 文件过大: %d bytes", fileInfo.Size()) http.Error(w, "File too large", http.StatusForbidden) return } // 打开文件 file, err := os.Open(filePath) if err != nil { log.Printf("[LocalFileHandler] 打开文件失败: %v", err) http.Error(w, fmt.Sprintf("Failed to open file: %v", err), http.StatusInternalServerError) return } defer file.Close() // 设置响应头 contentType := getContentType(ext) w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "public, max-age=3600") // 支持 Range 请求 w.Header().Set("Accept-Ranges", "bytes") // 获取文件信息(用于 Range 请求) fileStat, err := file.Stat() if err != nil { log.Printf("[LocalFileHandler] 获取文件信息失败: %v", err) http.Error(w, fmt.Sprintf("Failed to stat file: %v", err), http.StatusInternalServerError) return } // 使用 http.ServeContent 实现流式传输(支持 Range 请求) http.ServeContent(w, r, filepath.Base(filePath), fileStat.ModTime(), file) log.Printf("[LocalFileHandler] 文件传输成功: %s (%d bytes)", filePath, fileStat.Size()) } // LocalFileHandler 本地文件处理器(兼容旧代码) // 用于直接从文件系统提供文件,避免 base64 编码 type LocalFileHandler struct { http.Handler } // NewLocalFileHandler 创建本地文件处理器 func NewLocalFileHandler() *LocalFileHandler { // 启动本地文件服务器 go func() { if _, err := StartLocalFileServer(); err != nil { log.Printf("[LocalFileHandler] 启动本地文件服务器失败: %v", err) } }() return &LocalFileHandler{} } // ServeHTTP 处理 HTTP 请求(代理到 handleLocalFileRequest) func (h *LocalFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("[LocalFileHandler.ServeHTTP] 收到请求: %s (RawPath: %s)", r.URL.Path, r.URL.RawPath) // 检查是否是 /localfs/ 请求 if !strings.HasPrefix(r.URL.Path, "/localfs/") { log.Printf("[LocalFileHandler.ServeHTTP] 路径不匹配 /localfs/ 前缀,返回404") // 不是 /localfs/ 请求,返回 404 http.NotFound(w, r) return } // 直接调用实际的请求处理器 handleLocalFileRequest(w, r) } // getContentType 根据文件扩展名返回 MIME 类型 // 使用统一的文件类型管理器 func getContentType(ext string) string { return defaultFileTypeManager.GetMIMEType(ext) } // ReadFileAsBase64 读取文件并返回 base64 编码的字符串 // 用于读取从 ZIP 提取的临时图片文件 func ReadFileAsBase64(filePath string) (string, error) { log.Printf("[ReadFileAsBase64] 读取文件: %s", filePath) if !isSafePath(filePath) { return "", fmt.Errorf("路径不安全") } // 检查文件是否存在 fileInfo, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { return "", fmt.Errorf("文件不存在: %s", filePath) } return "", fmt.Errorf("无法访问文件: %v", err) } log.Printf("[ReadFileAsBase64] 文件大小: %d bytes", fileInfo.Size()) // 读取文件 data, err := os.ReadFile(filePath) if err != nil { return "", fmt.Errorf("读取文件失败: %v", err) } // 编码为 base64 encoded := base64.StdEncoding.EncodeToString(data) log.Printf("[ReadFileAsBase64] 编码成功: 原始=%d, base64=%d", len(data), len(encoded)) // 获取文件扩展名并确定 MIME 类型 ext := strings.ToLower(filepath.Ext(filePath)) mimeType := getContentType(ext) // 返回 data URI 格式: data:image/png;base64,iVBORw0KG... return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil } // HandleLocalFile 处理 /localfs/ 路由的 HTTP 请求 // 前端可以请求 http://localhost:18765/localfs/C:/path/to/image.jpg // 注意:此函数与 ServeHTTP 功能重复,建议统一使用 ServeHTTP func HandleLocalFile(w http.ResponseWriter, r *http.Request) { handler := NewLocalFileHandler() handler.ServeHTTP(w, r) } // isAllowedFileType 检查文件类型是否在白名单中 // 使用统一的文件类型管理器 func isAllowedFileType(ext string) bool { return defaultFileTypeManager.IsAllowed(ext) } // Shutdown 优雅关闭文件服务器 func (lfs *LocalFileServer) Shutdown() error { if lfs == nil || lfs.server == nil { return nil } lfs.mu.Lock() defer lfs.mu.Unlock() // 创建带超时的上下文 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() log.Printf("[LocalFileServer] 正在关闭...") if err := lfs.server.Shutdown(ctx); err != nil { log.Printf("[LocalFileServer] 关闭失败: %v", err) return err } log.Printf("[LocalFileServer] 已关闭") return nil } // ShutdownLocalFileServer 关闭全局文件服务器 func ShutdownLocalFileServer() error { if localFileServer != nil { return localFileServer.Shutdown() } return nil }