mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 09:18:51 +02:00
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:
@@ -3,10 +3,12 @@ package iconfetch
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -17,7 +19,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/vincent-petithory/dataurl"
|
"github.com/vincent-petithory/dataurl"
|
||||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
|
||||||
apitypes "github.com/yusing/goutils/apitypes"
|
apitypes "github.com/yusing/goutils/apitypes"
|
||||||
"github.com/yusing/goutils/cache"
|
"github.com/yusing/goutils/cache"
|
||||||
httputils "github.com/yusing/goutils/http"
|
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")
|
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) {
|
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (Result, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
|
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := gphttp.Do(req)
|
resp, err := fetchIconClient.Do(req)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
defer resp.Body.Close()
|
|
||||||
} else {
|
|
||||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||||
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
|
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
|
||||||
}
|
}
|
||||||
return FetchResultWithErrorf(http.StatusBadGateway, "connection error: %w", err)
|
return FetchResultWithErrorf(http.StatusBadGateway, "connection error: %w", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return FetchResultWithErrorf(resp.StatusCode, "upstream error: http %d", resp.StatusCode)
|
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)
|
icon, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FetchResultWithErrorf(http.StatusInternalServerError, "failed to read response body: %w", err)
|
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")
|
return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
res := Result{Icon: icon}
|
if ct == "" {
|
||||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
ct = http.DetectContentType(icon)
|
||||||
res.contentType = contentType
|
if !strings.HasPrefix(ct, "image/") {
|
||||||
|
return FetchResultWithErrorf(http.StatusNotFound, "not an image")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res := Result{Icon: icon}
|
||||||
|
res.contentType = ct
|
||||||
// else leave it empty
|
// else leave it empty
|
||||||
return res, nil
|
return res, nil
|
||||||
}).WithMaxEntries(200).WithRetriesExponentialBackoff(3).WithTTL(4 * time.Hour).Build()
|
}).WithMaxEntries(200).WithRetriesExponentialBackoff(3).WithTTL(4 * time.Hour).Build()
|
||||||
|
|||||||
Reference in New Issue
Block a user