Private
Public Access
1
0
Files
u-desk/internal/filesystem/asset_handler.go
绝尘 a6f99e0c49 修复:本地文件服务器 CORS 支持
问题:
- 前端运行在 http://wails.localhost
- 文件服务器运行在 http://localhost:18765
- 不同源导致 CORS 错误

修复:
- asset_handler.go 添加 CORS 响应头
- 支持 OPTIONS 预检请求
- 允许所有源访问(本地文件服务器)
2026-03-31 11:49:25 +08:00

307 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}