mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 09:18:51 +02:00
server side favicon retrieving and caching
This commit is contained in:
@@ -34,6 +34,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux.HandleFunc("GET", "/v1/schema/{filename...}", v1.GetSchemaFile)
|
||||
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
|
||||
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
|
||||
mux.HandleFunc("GET", "/v1/favicon/{alias}", auth.RequireAuth(v1.GetFavIcon))
|
||||
return mux
|
||||
}
|
||||
|
||||
|
||||
200
internal/api/v1/favicon.go
Normal file
200
internal/api/v1/favicon.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/vincent-petithory/dataurl"
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
)
|
||||
|
||||
type content struct {
|
||||
header http.Header
|
||||
data []byte
|
||||
status int
|
||||
}
|
||||
|
||||
func newContent() *content {
|
||||
return &content{
|
||||
header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *content) Header() http.Header {
|
||||
return c.header
|
||||
}
|
||||
|
||||
func (c *content) Write(data []byte) (int, error) {
|
||||
c.data = append(c.data, data...)
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (c *content) WriteHeader(statusCode int) {
|
||||
c.status = statusCode
|
||||
}
|
||||
|
||||
// GetFavIcon returns the favicon of the route
|
||||
//
|
||||
// Returns:
|
||||
// - 200 OK: if icon found
|
||||
// - 400 Bad Request: if alias is empty or route is not HTTPRoute
|
||||
// - 404 Not Found: if route or icon not found
|
||||
// - 500 Internal Server Error: if internal error
|
||||
// - others: depends on route handler response
|
||||
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
alias := req.PathValue("alias")
|
||||
if alias == "" {
|
||||
U.RespondError(w, U.ErrMissingKey("alias"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
r := listRoute(alias)
|
||||
if r == nil {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
switch r := r.(type) {
|
||||
case route.HTTPRoute:
|
||||
var icon []byte
|
||||
var status int
|
||||
var errMsg string
|
||||
|
||||
homepage := r.RawEntry().Homepage
|
||||
if homepage != nil && homepage.Icon != nil {
|
||||
if homepage.Icon.IsRelative {
|
||||
icon, status, errMsg = findIcon(r, req, homepage.Icon.Value)
|
||||
} else {
|
||||
icon, status, errMsg = getIconAbsolute(homepage.Icon.Value)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
icon, status, errMsg = findIcon(r, req, "/")
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
http.Error(w, errMsg, status)
|
||||
return
|
||||
}
|
||||
U.WriteBody(w, icon)
|
||||
default:
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// cache key can be absolute url or route name.
|
||||
var (
|
||||
iconCache = make(map[string][]byte)
|
||||
iconCacheMu sync.RWMutex
|
||||
)
|
||||
|
||||
func loadIconCache(key string) (icon []byte, ok bool) {
|
||||
iconCacheMu.RLock()
|
||||
icon, ok = iconCache[key]
|
||||
iconCacheMu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func storeIconCache(key string, icon []byte) {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
iconCache[key] = icon
|
||||
}
|
||||
|
||||
func getIconAbsolute(url string) ([]byte, int, string) {
|
||||
icon, ok := loadIconCache(url)
|
||||
if ok {
|
||||
return icon, http.StatusOK, ""
|
||||
}
|
||||
|
||||
resp, err := U.Get(url)
|
||||
if err != nil {
|
||||
storeIconCache(url, nil)
|
||||
logging.Error().Err(err).
|
||||
Str("url", url).
|
||||
Msg("failed to get icon")
|
||||
return nil, http.StatusBadGateway, "connection error"
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
icon, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
// storeIconCache(url, nil) // can retry
|
||||
logging.Error().Err(err).
|
||||
Str("url", url).
|
||||
Msg("failed to read icon")
|
||||
return nil, http.StatusInternalServerError, "internal error"
|
||||
}
|
||||
|
||||
storeIconCache(url, icon)
|
||||
return icon, http.StatusOK, ""
|
||||
}
|
||||
|
||||
func findIcon(r route.HTTPRoute, req *http.Request, path string) (icon []byte, status int, errMsg string) {
|
||||
key := r.TargetName()
|
||||
icon, ok := loadIconCache(key)
|
||||
if ok {
|
||||
if icon == nil {
|
||||
return nil, http.StatusNotFound, "icon not found"
|
||||
}
|
||||
return icon, http.StatusOK, ""
|
||||
}
|
||||
|
||||
icon, status, errMsg = findIconSlow(r, req, path)
|
||||
// set even if error (nil)
|
||||
storeIconCache(key, icon)
|
||||
return
|
||||
}
|
||||
|
||||
func findIconSlow(r route.HTTPRoute, req *http.Request, path string) (icon []byte, status int, errMsg string) {
|
||||
c := newContent()
|
||||
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
||||
defer cancel()
|
||||
newReq := req.WithContext(ctx)
|
||||
newReq.URL.Path = path
|
||||
newReq.URL.RawPath = path
|
||||
newReq.URL.RawQuery = ""
|
||||
newReq.RequestURI = path
|
||||
r.ServeHTTP(c, newReq)
|
||||
if c.status != http.StatusOK {
|
||||
return nil, c.status, "upstream error: " + http.StatusText(c.status)
|
||||
}
|
||||
// return icon data
|
||||
if path != "/" {
|
||||
return c.data, http.StatusOK, ""
|
||||
}
|
||||
// try extract from "link[rel=icon]" from path "/"
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data))
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, "internal error"
|
||||
}
|
||||
ele := doc.Find("link[rel=icon]").First()
|
||||
if ele.Length() == 0 {
|
||||
return nil, http.StatusNotFound, "icon not found"
|
||||
}
|
||||
href := ele.AttrOr("href", "")
|
||||
if href == "" {
|
||||
return nil, http.StatusNotFound, "icon not found"
|
||||
}
|
||||
// https://en.wikipedia.org/wiki/Data_URI_scheme
|
||||
if strings.HasPrefix(href, "data:image/") {
|
||||
dataURI, err := dataurl.DecodeString(href)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).
|
||||
Str("route", r.TargetName()).
|
||||
Msg("failed to decode favicon")
|
||||
return nil, http.StatusInternalServerError, "internal error"
|
||||
}
|
||||
return dataURI.Data, http.StatusOK, ""
|
||||
}
|
||||
if href[0] != '/' {
|
||||
return getIconAbsolute(href)
|
||||
}
|
||||
return findIconSlow(r, req, href)
|
||||
}
|
||||
Reference in New Issue
Block a user