refactor(icons): improve favicon fetching with custom HTTP client and content-type validation

Replace the existing HTTP client with a custom-configured client that skips TLS verification for favicon fetching,
and add explicit Content-Type validation to ensure only valid image responses are accepted.

This fixes potential issues with SSL certificate validation and prevents processing of non-image responses.
This commit is contained in:
yusing
2026-02-22 16:06:13 +08:00
parent bf54b51036
commit 4580543693

View File

@@ -3,10 +3,12 @@ package iconfetch
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"math"
"mime"
"net/http"
"net/url"
"slices"
@@ -17,7 +19,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/vincent-petithory/dataurl"
"github.com/yusing/godoxy/internal/homepage/icons"
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/cache"
httputils "github.com/yusing/goutils/http"
@@ -76,26 +77,42 @@ func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error
return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source")
}
var fetchIconClient = http.Client{
Timeout: faviconFetchTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (Result, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
}
resp, err := gphttp.Do(req)
if err == nil {
defer resp.Body.Close()
} else {
resp, err := fetchIconClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
}
return FetchResultWithErrorf(http.StatusBadGateway, "connection error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return FetchResultWithErrorf(resp.StatusCode, "upstream error: http %d", resp.StatusCode)
}
ct, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if ct != "" && !strings.HasPrefix(ct, "image/") {
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
}
icon, err := io.ReadAll(resp.Body)
if err != nil {
return FetchResultWithErrorf(http.StatusInternalServerError, "failed to read response body: %w", err)
@@ -105,10 +122,15 @@ var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (
return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
}
res := Result{Icon: icon}
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
res.contentType = contentType
if ct == "" {
ct = http.DetectContentType(icon)
if !strings.HasPrefix(ct, "image/") {
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
}
}
res := Result{Icon: icon}
res.contentType = ct
// else leave it empty
return res, nil
}).WithMaxEntries(200).WithRetriesExponentialBackoff(3).WithTTL(4 * time.Hour).Build()