Files
code-utils/imgproxy.go
2026-08-15 17:18:00 +08:00

158 lines
4.3 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 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
}