问题: - 前端运行在 http://wails.localhost - 文件服务器运行在 http://localhost:18765 - 不同源导致 CORS 错误 修复: - asset_handler.go 添加 CORS 响应头 - 支持 OPTIONS 预检请求 - 允许所有源访问(本地文件服务器)
307 lines
8.8 KiB
Go
307 lines
8.8 KiB
Go
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
|
||
}
|