Files
code-utils/imgproxy.go

158 lines
4.3 KiB
Go
Raw Normal View History

2026-08-15 17:18:00 +08:00
package main
// imgproxy.go远程图片拉取。
// WebView 页面源是 http://wails.localhost直接加载外站 HTTPS 图常会破图。
// 优先经 Go 拉成 dataURLFetchRemoteImageAsDataURLAssetServer /__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=<urlencoded http(s) URL>,拉取上游图片并回传。
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
}