package main // imgproxy.go:远程图片拉取。 // WebView 页面源是 http://wails.localhost,直接加载外站 HTTPS 图常会破图。 // 优先经 Go 拉成 dataURL(FetchRemoteImageAsDataURL);AssetServer /__ccimg 作兜底。 import ( "context" "encoding/base64" "errors" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" ) const ( imgProxyPath = "/__ccimg" imgProxyMaxBytes = 12 << 20 // 12MB ) var imgProxyClient = &http.Client{Timeout: 20 * time.Second} var ( imgDataURLMu sync.Mutex imgDataURLCache = map[string]string{} errBadImageURL = errors.New("bad url") ) // remoteImageMiddleware 拦截 /__ccimg?u=,拉取上游图片并回传。 func remoteImageMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := strings.TrimSuffix(r.URL.Path, "/") if path != imgProxyPath { next.ServeHTTP(w, r) return } raw := strings.TrimSpace(r.URL.Query().Get("u")) data, ct, err := fetchRemoteImage(r.Context(), raw) if err != nil { code := http.StatusBadGateway if errors.Is(err, errBadImageURL) { code = http.StatusBadRequest } http.Error(w, err.Error(), code) return } w.Header().Set("Content-Type", ct) w.Header().Set("Cache-Control", "private, max-age=86400") _, _ = w.Write(data) }) } func fetchRemoteImage(ctx context.Context, raw string) ([]byte, string, error) { raw = strings.TrimSpace(raw) u, err := url.Parse(raw) if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { return nil, "", errBadImageURL } if ctx == nil { ctx = context.Background() } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, "", errBadImageURL } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") req.Header.Set("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8") resp, err := imgProxyClient.Do(req) if err != nil { return nil, "", errors.New("upstream unreachable") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, "", errors.New("upstream error") } ct := resp.Header.Get("Content-Type") if i := strings.Index(ct, ";"); i >= 0 { ct = strings.TrimSpace(ct[:i]) } data, err := io.ReadAll(io.LimitReader(resp.Body, imgProxyMaxBytes+1)) if err != nil { return nil, "", errors.New("upstream read failed") } if len(data) > imgProxyMaxBytes { return nil, "", errors.New("image too large") } if len(data) == 0 { return nil, "", errors.New("empty image") } if ct == "" || (!strings.HasPrefix(ct, "image/") && !strings.Contains(ct, "octet-stream")) { if sniffed := sniffImageMIME(data); sniffed != "" { ct = sniffed } } if ct == "" { ct = "application/octet-stream" } return data, ct, nil } func sniffImageMIME(b []byte) string { if len(b) >= 3 && b[0] == 0xff && b[1] == 0xd8 && b[2] == 0xff { return "image/jpeg" } if len(b) >= 8 && string(b[:8]) == "\x89PNG\r\n\x1a\n" { return "image/png" } if len(b) >= 6 && (string(b[:6]) == "GIF87a" || string(b[:6]) == "GIF89a") { return "image/gif" } if len(b) >= 12 && string(b[:4]) == "RIFF" && string(b[8:12]) == "WEBP" { return "image/webp" } return "" } // FetchRemoteImageAsDataURL 由 Go 拉取远程图片并返回 dataURL,绕过 WebView 外站限制。 func (a *App) FetchRemoteImageAsDataURL(rawURL string) (string, error) { rawURL = strings.TrimSpace(rawURL) if rawURL == "" { return "", errors.New("EMPTY_URL") } if strings.HasPrefix(rawURL, "data:") { return rawURL, nil } imgDataURLMu.Lock() if cached, ok := imgDataURLCache[rawURL]; ok { imgDataURLMu.Unlock() return cached, nil } imgDataURLMu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() data, ct, err := fetchRemoteImage(ctx, rawURL) if err != nil { return "", err } if !strings.HasPrefix(ct, "image/") { ct = "image/png" if sniffed := sniffImageMIME(data); sniffed != "" { ct = sniffed } } out := fmt.Sprintf("data:%s;base64,%s", ct, base64.StdEncoding.EncodeToString(data)) imgDataURLMu.Lock() if len(imgDataURLCache) > 128 { imgDataURLCache = map[string]string{} } imgDataURLCache[rawURL] = out imgDataURLMu.Unlock() return out, nil }