更新若干功能
This commit is contained in:
157
imgproxy.go
Normal file
157
imgproxy.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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=<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
|
||||
}
|
||||
Reference in New Issue
Block a user