From 45805436932d0cd8cb162dc5db937bfa80b8efe4 Mon Sep 17 00:00:00 2001 From: yusing Date: Sun, 22 Feb 2026 16:06:13 +0800 Subject: [PATCH] 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. --- internal/homepage/icons/fetch/fetch.go | 38 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/internal/homepage/icons/fetch/fetch.go b/internal/homepage/icons/fetch/fetch.go index 67b4b4ea..af2c8cbf 100644 --- a/internal/homepage/icons/fetch/fetch.go +++ b/internal/homepage/icons/fetch/fetch.go @@ -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()