From 6a5d3247334f488a704b7f358dd3436229d2ec2e Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 10 Apr 2025 06:04:14 +0800 Subject: [PATCH] refactor: move favicon into homepage module --- cmd/main.go | 8 +- internal/api/handler.go | 3 +- internal/api/v1/favicon.go | 77 +++++ internal/api/v1/favicon/favicon.go | 284 ------------------ internal/api/v1/list.go | 4 +- .../{api/v1/favicon => homepage}/content.go | 2 +- internal/homepage/favicon.go | 195 ++++++++++++ .../cache.go => homepage/icon_cache.go} | 11 +- internal/homepage/icon_url.go | 5 +- internal/{ => homepage}/list-icons.go | 20 +- internal/homepage/route.go | 19 ++ internal/route/reverse_proxy.go | 2 +- internal/route/route.go | 25 +- internal/route/types/route.go | 1 + 14 files changed, 328 insertions(+), 328 deletions(-) create mode 100644 internal/api/v1/favicon.go delete mode 100644 internal/api/v1/favicon/favicon.go rename internal/{api/v1/favicon => homepage}/content.go (97%) create mode 100644 internal/homepage/favicon.go rename internal/{api/v1/favicon/cache.go => homepage/icon_cache.go} (91%) rename internal/{ => homepage}/list-icons.go (94%) create mode 100644 internal/homepage/route.go diff --git a/cmd/main.go b/cmd/main.go index d93179a4..f1461db2 100755 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,9 +6,7 @@ import ( "os" "sync" - "github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal/api/v1/auth" - "github.com/yusing/go-proxy/internal/api/v1/favicon" "github.com/yusing/go-proxy/internal/api/v1/query" "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/config" @@ -50,7 +48,7 @@ func main() { rawLogger.Println("ok") return case common.CommandListIcons: - icons, err := internal.ListAvailableIcons() + icons, err := homepage.ListAvailableIcons() if err != nil { rawLogger.Fatal(err) } @@ -79,9 +77,9 @@ func main() { logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion()) logging.Trace().Msg("trace enabled") parallel( - internal.InitIconListCache, + homepage.InitIconListCache, + homepage.InitIconCache, homepage.InitOverridesConfig, - favicon.InitIconCache, systeminfo.Poller.Start, ) diff --git a/internal/api/handler.go b/internal/api/handler.go index a16e19bf..e1d86db7 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -9,7 +9,6 @@ import ( "github.com/yusing/go-proxy/internal/api/v1/auth" "github.com/yusing/go-proxy/internal/api/v1/certapi" "github.com/yusing/go-proxy/internal/api/v1/dockerapi" - "github.com/yusing/go-proxy/internal/api/v1/favicon" "github.com/yusing/go-proxy/internal/common" config "github.com/yusing/go-proxy/internal/config/types" "github.com/yusing/go-proxy/internal/logging" @@ -80,7 +79,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler { mux.HandleFunc("POST", "/v1/file/validate/{type}", v1.ValidateFile, true) mux.HandleFunc("GET", "/v1/health", v1.Health, true) mux.HandleFunc("GET", "/v1/logs", memlogger.Handler(), true) - mux.HandleFunc("GET", "/v1/favicon", favicon.GetFavIcon, true) + mux.HandleFunc("GET", "/v1/favicon", v1.GetFavIcon, true) mux.HandleFunc("POST", "/v1/homepage/set", v1.SetHomePageOverrides, true) mux.HandleFunc("GET", "/v1/agents", v1.ListAgents, true) mux.HandleFunc("GET", "/v1/agents/new", v1.NewAgent, true) diff --git a/internal/api/v1/favicon.go b/internal/api/v1/favicon.go new file mode 100644 index 00000000..8d314f65 --- /dev/null +++ b/internal/api/v1/favicon.go @@ -0,0 +1,77 @@ +package v1 + +import ( + "errors" + "net/http" + + "github.com/yusing/go-proxy/internal/gperr" + "github.com/yusing/go-proxy/internal/homepage" + "github.com/yusing/go-proxy/internal/net/gphttp" + "github.com/yusing/go-proxy/internal/route/routes" +) + +// 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) { + url, alias := req.FormValue("url"), req.FormValue("alias") + if url == "" && alias == "" { + gphttp.ClientError(w, gphttp.ErrMissingKey("url or alias"), http.StatusBadRequest) + return + } + if url != "" && alias != "" { + gphttp.ClientError(w, gperr.New("url and alias are mutually exclusive"), http.StatusBadRequest) + return + } + + // try with url + if url != "" { + var iconURL homepage.IconURL + if err := iconURL.Parse(url); err != nil { + gphttp.ClientError(w, err, http.StatusBadRequest) + return + } + fetchResult := homepage.FetchFavIconFromURL(&iconURL) + if !fetchResult.OK() { + http.Error(w, fetchResult.ErrMsg, fetchResult.StatusCode) + return + } + w.Header().Set("Content-Type", fetchResult.ContentType()) + gphttp.WriteBody(w, fetchResult.Icon) + return + } + + // try with route.Icon + r, ok := routes.GetHTTPRoute(alias) + if !ok { + gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound) + return + } + + var result *homepage.FetchResult + hp := r.HomepageItem() + if hp.Icon != nil { + if hp.Icon.IconSource == homepage.IconSourceRelative { + result = homepage.FindIcon(req.Context(), r, hp.Icon.Value) + } else { + result = homepage.FetchFavIconFromURL(hp.Icon) + } + } else { + // try extract from "link[rel=icon]" + result = homepage.FindIcon(req.Context(), r, "/") + } + if result.StatusCode == 0 { + result.StatusCode = http.StatusOK + } + if !result.OK() { + http.Error(w, result.ErrMsg, result.StatusCode) + return + } + w.Header().Set("Content-Type", result.ContentType()) + gphttp.WriteBody(w, result.Icon) +} diff --git a/internal/api/v1/favicon/favicon.go b/internal/api/v1/favicon/favicon.go deleted file mode 100644 index ce38b1ee..00000000 --- a/internal/api/v1/favicon/favicon.go +++ /dev/null @@ -1,284 +0,0 @@ -package favicon - -import ( - "bytes" - "context" - "errors" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/PuerkitoBio/goquery" - "github.com/vincent-petithory/dataurl" - "github.com/yusing/go-proxy/internal/gperr" - "github.com/yusing/go-proxy/internal/homepage" - "github.com/yusing/go-proxy/internal/logging" - gphttp "github.com/yusing/go-proxy/internal/net/gphttp" - "github.com/yusing/go-proxy/internal/route/routes" - route "github.com/yusing/go-proxy/internal/route/types" - "github.com/yusing/go-proxy/internal/utils/strutils" -) - -type fetchResult struct { - icon []byte - contentType string - statusCode int - errMsg string -} - -func (res *fetchResult) OK() bool { - return res.icon != nil -} - -func (res *fetchResult) ContentType() string { - if res.contentType == "" { - if bytes.HasPrefix(res.icon, []byte(" MaxRedirectDepth { - return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "too many redirects"} - } - loc = strutils.SanitizeURI(loc) - if loc == "/" || loc == newReq.URL.Path { - return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "circular redirect"} - } - return findIconSlow(r, req, loc, depth+1) - } - } - return &fetchResult{statusCode: c.status, errMsg: "upstream error: " + string(c.data)} - } - // return icon data - if !gphttp.GetContentType(c.header).IsHTML() { - return &fetchResult{icon: c.data, contentType: c.header.Get("Content-Type")} - } - // try extract from "link[rel=icon]" from path "/" - doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data)) - if err != nil { - logging.Error().Err(err). - Str("route", r.TargetName()). - Msg("failed to parse html") - return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"} - } - ele := doc.Find("head > link[rel=icon]").First() - if ele.Length() == 0 { - return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon element not found"} - } - href := ele.AttrOr("href", "") - if href == "" { - return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon href 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 &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"} - } - return &fetchResult{icon: dataURI.Data, contentType: dataURI.ContentType()} - } - switch { - case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"): - return fetchIconAbsolute(href) - default: - return findIconSlow(r, req, href, 0) - } -} diff --git a/internal/api/v1/list.go b/internal/api/v1/list.go index ecbce0d6..ef6f95f8 100644 --- a/internal/api/v1/list.go +++ b/internal/api/v1/list.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal/common" config "github.com/yusing/go-proxy/internal/config/types" + "github.com/yusing/go-proxy/internal/homepage" "github.com/yusing/go-proxy/internal/net/gphttp" "github.com/yusing/go-proxy/internal/net/gphttp/middleware" "github.com/yusing/go-proxy/internal/route/routes/routequery" @@ -67,7 +67,7 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) { if err != nil { limit = 0 } - icons, err := internal.SearchIcons(r.FormValue("keyword"), limit) + icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit) if err != nil { gphttp.ClientError(w, err) return diff --git a/internal/api/v1/favicon/content.go b/internal/homepage/content.go similarity index 97% rename from internal/api/v1/favicon/content.go rename to internal/homepage/content.go index 5d4ba3e3..fa0ecdce 100644 --- a/internal/api/v1/favicon/content.go +++ b/internal/homepage/content.go @@ -1,4 +1,4 @@ -package favicon +package homepage import ( "bufio" diff --git a/internal/homepage/favicon.go b/internal/homepage/favicon.go new file mode 100644 index 00000000..e355b63f --- /dev/null +++ b/internal/homepage/favicon.go @@ -0,0 +1,195 @@ +package homepage + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/vincent-petithory/dataurl" + gphttp "github.com/yusing/go-proxy/internal/net/gphttp" + "github.com/yusing/go-proxy/internal/utils/strutils" +) + +type FetchResult struct { + Icon []byte + StatusCode int + ErrMsg string + + contentType string +} + +func (res *FetchResult) OK() bool { + return res.Icon != nil +} + +func (res *FetchResult) ContentType() string { + if res.contentType == "" { + if bytes.HasPrefix(res.Icon, []byte(" maxRedirectDepth { + return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "too many redirects"} + } + loc = strutils.SanitizeURI(loc) + if loc == "/" || loc == newReq.URL.Path { + return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "circular redirect"} + } + return findIconSlow(ctx, r, loc, depth+1) + } + } + return &FetchResult{StatusCode: c.status, ErrMsg: "upstream error: " + string(c.data)} + } + // return icon data + if !gphttp.GetContentType(c.header).IsHTML() { + return &FetchResult{Icon: c.data, contentType: c.header.Get("Content-Type")} + } + // try extract from "link[rel=icon]" from path "/" + doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data)) + if err != nil { + return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "failed to parse html"} + } + ele := doc.Find("head > link[rel=icon]").First() + if ele.Length() == 0 { + return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "icon element not found"} + } + href := ele.AttrOr("href", "") + if href == "" { + return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "icon href not found"} + } + // https://en.wikipedia.org/wiki/Data_URI_scheme + if strings.HasPrefix(href, "data:image/") { + dataURI, err := dataurl.DecodeString(href) + if err != nil { + return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "failed to decode favicon"} + } + return &FetchResult{Icon: dataURI.Data, contentType: dataURI.ContentType()} + } + switch { + case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"): + return fetchIconAbsolute(href) + default: + return findIconSlow(ctx, r, href, 0) + } +} diff --git a/internal/api/v1/favicon/cache.go b/internal/homepage/icon_cache.go similarity index 91% rename from internal/api/v1/favicon/cache.go rename to internal/homepage/icon_cache.go index b6764a80..b7f68ea2 100644 --- a/internal/api/v1/favicon/cache.go +++ b/internal/homepage/icon_cache.go @@ -1,4 +1,4 @@ -package favicon +package homepage import ( "encoding/json" @@ -7,7 +7,6 @@ import ( "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/logging" - route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/utils" ) @@ -82,17 +81,17 @@ func pruneExpiredIconCache() { } } -func routeKey(r route.HTTPRoute) string { +func routeKey(r route) string { return r.ProviderName() + ":" + r.TargetName() } -func PruneRouteIconCache(route route.HTTPRoute) { +func PruneRouteIconCache(route route) { iconCacheMu.Lock() defer iconCacheMu.Unlock() delete(iconCache, routeKey(route)) } -func loadIconCache(key string) *fetchResult { +func loadIconCache(key string) *FetchResult { iconCacheMu.RLock() defer iconCacheMu.RUnlock() @@ -102,7 +101,7 @@ func loadIconCache(key string) *fetchResult { Str("key", key). Msg("icon found in cache") icon.LastAccess = time.Now() - return &fetchResult{icon: icon.Icon} + return &FetchResult{Icon: icon.Icon} } return nil } diff --git a/internal/homepage/icon_url.go b/internal/homepage/icon_url.go index f53060d6..b6847aaf 100644 --- a/internal/homepage/icon_url.go +++ b/internal/homepage/icon_url.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/yusing/go-proxy/internal" "github.com/yusing/go-proxy/internal/gperr" ) @@ -62,10 +61,10 @@ func NewWalkXCodeIconURL(name, format string) *IconURL { // otherwise returns true. func (u *IconURL) HasIcon() bool { if u.IconSource == IconSourceSelfhSt { - return internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType) + return HasSelfhstIcon(u.Extra.Name, u.Extra.FileType) } if u.IconSource == IconSourceWalkXCode { - return internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType) + return HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType) } return true } diff --git a/internal/list-icons.go b/internal/homepage/list-icons.go similarity index 94% rename from internal/list-icons.go rename to internal/homepage/list-icons.go index 1f3d2c60..a76a87e6 100644 --- a/internal/list-icons.go +++ b/internal/homepage/list-icons.go @@ -1,4 +1,4 @@ -package internal +package homepage import ( "encoding/json" @@ -59,15 +59,15 @@ func InitIconListCache() { DisplayNames: make(ReferenceDisplayNameMap), IconList: []string{}, } - err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache) - if err != nil { - logging.Error().Err(err).Msg("failed to load icon list cache config") - } else if len(iconsCache.IconList) > 0 { - logging.Info(). - Int("icons", len(iconsCache.IconList)). - Int("display_names", len(iconsCache.DisplayNames)). - Msg("icon list cache loaded") - } + // err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache) + // if err != nil { + // logging.Error().Err(err).Msg("failed to load icon list cache config") + // } else if len(iconsCache.IconList) > 0 { + // logging.Info(). + // Int("icons", len(iconsCache.IconList)). + // Int("display_names", len(iconsCache.DisplayNames)). + // Msg("icon list cache loaded") + // } } func ListAvailableIcons() (*Cache, error) { diff --git a/internal/homepage/route.go b/internal/homepage/route.go new file mode 100644 index 00000000..d48cf712 --- /dev/null +++ b/internal/homepage/route.go @@ -0,0 +1,19 @@ +package homepage + +import ( + "net/http" + + net "github.com/yusing/go-proxy/internal/net/types" +) + +type route interface { + TargetName() string + ProviderName() string + Reference() string + TargetURL() *net.URL +} + +type httpRoute interface { + route + http.Handler +} diff --git a/internal/route/reverse_proxy.go b/internal/route/reverse_proxy.go index d08551d1..bd03fc14 100755 --- a/internal/route/reverse_proxy.go +++ b/internal/route/reverse_proxy.go @@ -173,7 +173,7 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error { }) } - r.task.OnCancel("reset_favicon", func() { favicon.PruneRouteIconCache(r) }) + r.task.OnCancel("reset_favicon", func() { homepage.PruneRouteIconCache(r) }) return nil } diff --git a/internal/route/route.go b/internal/route/route.go index eba7f4ec..c1872900 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -147,6 +147,13 @@ func (r *Route) Started() bool { return r.impl != nil } +func (r *Route) Reference() string { + if r.Docker != nil { + return r.Docker.Image.Name + } + return r.Alias +} + func (r *Route) ProviderName() string { return r.Provider } @@ -390,21 +397,16 @@ func (r *Route) FinalizeHomepageConfig() { r.Homepage = r.Homepage.GetOverride(r.Alias) hp := r.Homepage + ref := r.Reference() - var key string if hp.Name == "" { - if r.Container != nil { - key = r.Container.Image.Name - } else { - key = r.Alias - } - displayName, ok := internal.GetDisplayName(key) + displayName, ok := homepage.GetDisplayName(ref) if ok { hp.Name = displayName } else { hp.Name = strutils.Title( strings.ReplaceAll( - strings.ReplaceAll(key, "-", " "), + strings.ReplaceAll(r.Alias, "-", " "), "_", " ", ), ) @@ -413,12 +415,7 @@ func (r *Route) FinalizeHomepageConfig() { if hp.Category == "" { if config.GetInstance().Value().Homepage.UseDefaultCategories { - if isDocker { - key = r.Container.Image.Name - } else { - key = strings.ToLower(r.Alias) - } - if category, ok := homepage.PredefinedCategories[key]; ok { + if category, ok := homepage.PredefinedCategories[ref]; ok { hp.Category = category } } diff --git a/internal/route/types/route.go b/internal/route/types/route.go index 9dc9212b..d9be0ff6 100644 --- a/internal/route/types/route.go +++ b/internal/route/types/route.go @@ -24,6 +24,7 @@ type ( TargetName() string TargetURL() *net.URL HealthMonitor() health.HealthMonitor + Reference() string Started() bool