diff --git a/changelog.md b/changelog.md index 949c266..32a7520 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ - Added Button to force reset HTTPS cert in settings - New color slider with reset buttons - Added a notification when updating a container + - Improved icon loading speed, and added proper placeholder - Added lazyloading to URL and Servapp pages images - Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features - Added a button in the servapp page to easily download the docker backup diff --git a/client/src/components/imageWithPlaceholder.jsx b/client/src/components/imageWithPlaceholder.jsx new file mode 100644 index 0000000..ffc4e2a --- /dev/null +++ b/client/src/components/imageWithPlaceholder.jsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import LazyLoad from 'react-lazyload'; +import cosmosGray from '../assets/images/icons/cosmos_gray.png'; + +function ImageWithPlaceholder({ src, alt, placeholder, ...props }) { + const [imageSrc, setImageSrc] = useState(placeholder || cosmosGray); + const [imageRef, setImageRef] = useState(); + + const onLoad = event => { + event.target.classList.add('loaded'); + }; + + const onError = () => { + setImageSrc(cosmosGray); + }; + + // This function will be called when the actual image is loaded + const handleImageLoad = () => { + if (imageRef) { + imageRef.src = src; + } + }; + + return ( + <> + {alt} + {/* This image will load the actual image and then handleImageLoad will be triggered */} + {alt} + + ); +} + +export default ImageWithPlaceholder; \ No newline at end of file diff --git a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx index d031200..05712fe 100644 --- a/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx +++ b/client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx @@ -64,18 +64,22 @@ const Notification = () => { const setAsRead = () => { let unread = []; - let newN = notifications.map((notif) => { + notifications.forEach((notif) => { if (!notif.Read) { unread.push(notif.ID); } - notif.Read = true; - return notif; }) if (unread.length > 0) { API.users.readNotifs(unread); } + } + const setLocalAsRead = (id) => { + let newN = notifications.map((notif) => { + notif.Read = true; + return notif; + }) setNotifications(newN); } @@ -104,6 +108,7 @@ const Notification = () => { if (anchorRef.current && anchorRef.current.contains(event.target)) { return; } + setLocalAsRead(); setOpen(false); }; diff --git a/client/src/pages/config/routes/routeoverview.jsx b/client/src/pages/config/routes/routeoverview.jsx index f4a0ee2..ae2e90e 100644 --- a/client/src/pages/config/routes/routeoverview.jsx +++ b/client/src/pages/config/routes/routeoverview.jsx @@ -12,6 +12,7 @@ import { redirectToLocal } from '../../../utils/indexs'; import { CosmosCheckbox } from '../users/formShortcuts'; import { Field } from 'formik'; import MiniPlotComponent from '../../dashboard/components/mini-plot'; +import ImageWithPlaceholder from '../../../components/imageWithPlaceholder'; const info = { backgroundColor: 'rgba(0, 0, 0, 0.1)', @@ -39,7 +40,7 @@ const RouteOverview = ({ routeConfig }) => { }>
- +
Description diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx index e3583e9..1407a0c 100644 --- a/client/src/pages/config/users/proxyman.jsx +++ b/client/src/pages/config/users/proxyman.jsx @@ -41,6 +41,7 @@ import { useNavigate } from 'react-router'; import NewRouteCreate from '../routes/newRoute'; import LazyLoad from 'react-lazyload'; import MiniPlotComponent from '../../dashboard/components/mini-plot'; +import ImageWithPlaceholder from '../../../components/imageWithPlaceholder'; const stickyButton = { position: 'fixed', @@ -166,7 +167,7 @@ const ProxyManagement = () => { { title: '', field: (r) => - + , style: { textAlign: 'center', diff --git a/client/src/pages/dashboard/components/mini-plot.jsx b/client/src/pages/dashboard/components/mini-plot.jsx index 53c08c0..2518741 100644 --- a/client/src/pages/dashboard/components/mini-plot.jsx +++ b/client/src/pages/dashboard/components/mini-plot.jsx @@ -39,7 +39,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit const [ref, inView] = useInView(); useEffect(() => { - if(!inView) return; + if(!inView || series.length) return; let xAxis = []; @@ -202,7 +202,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit alignItems='center' sx={{padding: '0px 20px', width: '100%', backgroundColor: noBackground ? '' : 'rgba(0,0,0,0.075)'}} justifyContent={'space-around'}> - diff --git a/client/src/utils/servapp-icon.jsx b/client/src/utils/servapp-icon.jsx index 2ee0e74..02e4766 100644 --- a/client/src/utils/servapp-icon.jsx +++ b/client/src/utils/servapp-icon.jsx @@ -1,12 +1,13 @@ import { getFaviconURL } from "./routes"; import logogray from '../assets/images/icons/cosmos_gray.png'; import LazyLoad from 'react-lazyload'; +import ImageWithPlaceholder from "../components/imageWithPlaceholder"; export const ServAppIcon = ({route, container, width, ...pprops}) => { return {(container && container.Labels["cosmos-icon"]) ? - :( - route ? - : )} + :( + route ? + : )} ; }; \ No newline at end of file diff --git a/package.json b/package.json index c8c858e..d84b8c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.12.0-unstable41", + "version": "0.12.0-unstable42", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/httpServer.go b/src/httpServer.go index 6675fa6..537a962 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -284,8 +284,11 @@ func InitServer() *mux.Router { router.Use(utils.BlockByCountryMiddleware(config.BlockedCountries, config.CountryBlacklistIsWhitelist)) } - router.HandleFunc("/logo", SendLogo) - + logoAPI := router.PathPrefix("/logo").Subrouter() + SecureAPI(logoAPI, true) + logoAPI.HandleFunc("/", SendLogo) + + srapi := router.PathPrefix("/cosmos").Subrouter() srapi.HandleFunc("/api/dns", GetDNSRoute) diff --git a/src/icons.go b/src/icons.go index 953e290..6c166fa 100644 --- a/src/icons.go +++ b/src/icons.go @@ -11,6 +11,7 @@ import ( "path" "time" "context" + "sync" "go.deanishe.net/favicon" @@ -69,154 +70,185 @@ func sendFallback(w http.ResponseWriter) { } var IconCacheLock = make(chan bool, 1) - +type result struct { + IconURL string + CachedImage CachedImage + Error error +} func GetFavicon(w http.ResponseWriter, req *http.Request) { if utils.LoggedInOnly(w, req) != nil { - return + return } // get url from query string escsiteurl := req.URL.Query().Get("q") - + IconCacheLock <- true defer func() { <-IconCacheLock }() - + // URL decode siteurl, err := url.QueryUnescape(escsiteurl) if err != nil { - utils.Error("Favicon: URL decode", err) - utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002") - return + utils.Error("Favicon: URL decode", err) + utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002") + return } - if(req.Method == "GET") { - utils.Log("Fetch favicon for " + siteurl) + if req.Method == "GET" { + utils.Log("Fetch favicon for " + siteurl) - // Check if we have the favicon in cache - if _, ok := IconCache[siteurl]; ok { - utils.Debug("Favicon in cache") - resp := IconCache[siteurl] - sendImage(w, resp) - return - } - - var icons []*favicon.Icon - var defaultIcons = []*favicon.Icon{ - &favicon.Icon{URL: "favicon.png", Width: 0}, - &favicon.Icon{URL: "/favicon.png", Width: 0}, - &favicon.Icon{URL: "favicon.ico", Width: 0}, - &favicon.Icon{URL: "/favicon.ico", Width: 0}, - } - - // follow siteurl and check if any redirect. - - respNew, err := httpGetWithTimeout(siteurl) - - if err != nil { - utils.Error("FaviconFetch", err) - icons = append(icons, defaultIcons...) - } else { - siteurl = respNew.Request.URL.String() - icons, err = favicon.Find(siteurl) - - if err != nil || len(icons) == 0 { - icons = append(icons, defaultIcons...) - } else { - // Check if icons list is missing any default values - for _, defaultIcon := range defaultIcons { - found := false - for _, icon := range icons { - if icon.URL == defaultIcon.URL { - found = true - break - } - } - if !found { - icons = append(icons, defaultIcon) - } - } + // Check if we have the favicon in cache + if resp, ok := IconCache[siteurl]; ok { + utils.Debug("Favicon in cache") + sendImage(w, resp) + return } - } - for _, icon := range icons { - if icon.Width <= 256 { + var icons []*favicon.Icon + var defaultIcons = []*favicon.Icon{ + {URL: "/favicon.ico", Width: 0}, + {URL: "/favicon.png", Width: 0}, + {URL: "favicon.ico", Width: 0}, + {URL: "favicon.png", Width: 0}, + } - iconURL := icon.URL - u, err := url.Parse(siteurl) - if err != nil { - utils.Debug("FaviconFetch failed to parse " + err.Error()) - continue - } - - if !strings.HasPrefix(iconURL, "http") { - if strings.HasPrefix(iconURL, ".") { - // Relative URL starting with "." - // Resolve the relative URL based on the base URL - baseURL := u.Scheme + "://" + u.Host - iconURL = baseURL + iconURL[1:] - } else if strings.HasPrefix(iconURL, "/") { - // Relative URL starting with "/" - // Append the relative URL to the base URL - iconURL = u.Scheme + "://" + u.Host + iconURL + // follow siteurl and check if any redirect. + respNew, err := httpGetWithTimeout(siteurl) + if err != nil { + utils.Error("FaviconFetch", err) + icons = append(icons, defaultIcons...) + } else { + siteurl = respNew.Request.URL.String() + icons, err = favicon.Find(siteurl) + + if err != nil || len(icons) == 0 { + icons = append(icons, defaultIcons...) } else { - // Relative URL without starting dot or slash - // Construct the absolute URL based on the current page's URL path - baseURL := u.Scheme + "://" + u.Host - baseURLPath := path.Dir(u.Path) - iconURL = baseURL + baseURLPath + "/" + iconURL + // Check if icons list is missing any default values + for _, defaultIcon := range defaultIcons { + found := false + for _, icon := range icons { + if icon.URL == defaultIcon.URL { + found = true + break + } + } + if !found { + icons = append(icons, defaultIcon) + } + } } - } - - utils.Debug("Favicon Trying to fetch " + iconURL) + } - // Fetch the favicon - resp, err := httpGetWithTimeout(iconURL) - if err != nil { - utils.Debug("FaviconFetch - " + err.Error()) - continue - } + // Create a channel to collect favicon fetch results + resultsChan := make(chan result) + // Create a wait group to wait for all goroutines to finish + var wg sync.WaitGroup - // check if 200 and if image - if resp.StatusCode != 200 { - utils.Debug("FaviconFetch - " + iconURL + " - not 200 ") - continue - } else if !strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream") { - utils.Debug("FaviconFetch - " + iconURL + " - not image ") - continue - } else { - utils.Log("Favicon found " + iconURL) - - // Cache the response - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - utils.Debug("FaviconFetch - cant read " + err.Error()) - continue - } - - finalImage := CachedImage{ - ContentType: resp.Header.Get("Content-Type"), - ETag: resp.Header.Get("ETag"), - Body: body, + // Loop through each icon and start a goroutine to fetch it + for _, icon := range icons { + if icon.Width <= 256 { + wg.Add(1) + go func(icon *favicon.Icon) { + defer wg.Done() + fetchAndCacheIcon(icon, siteurl, resultsChan) + }(icon) } + } - IconCache[siteurl] = finalImage + // Close the results channel when all fetches are done + go func() { + wg.Wait() + close(resultsChan) + }() + // Collect the results + for result := range resultsChan { + IconCache[siteurl] = result.CachedImage sendImage(w, IconCache[siteurl]) return - } } - } - utils.Log("Favicon final fallback") - sendFallback(w) - return - + + utils.Log("Favicon final fallback") + sendFallback(w) + return + } else { - utils.Error("Favicon: Method not allowed" + req.Method, nil) - utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") - return + utils.Error("Favicon: Method not allowed "+req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return } } +// fetchAndCacheIcon is a helper function to fetch and cache the icon +func fetchAndCacheIcon(icon *favicon.Icon, baseSiteURL string, resultsChan chan<- result) { + iconURL := icon.URL + u, err := url.Parse(baseSiteURL) + if err != nil { + utils.Debug("FaviconFetch failed to parse " + err.Error()) + return + } + + if !strings.HasPrefix(iconURL, "http") { + // Process the iconURL to make it absolute + iconURL = resolveIconURL(iconURL, u) + } + + utils.Debug("Favicon Trying to fetch " + iconURL) + + // Fetch the favicon + resp, err := httpGetWithTimeout(iconURL) + if err != nil { + utils.Debug("FaviconFetch - " + err.Error()) + return + } + defer resp.Body.Close() + + // Check if response is successful and content type is image + if resp.StatusCode != 200 || (!strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream")) { + utils.Debug("FaviconFetch - " + iconURL + " - not 200 or not image ") + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + utils.Debug("FaviconFetch - can't read " + err.Error()) + return + } + + // Prepare the cached image + cachedImage := CachedImage{ + ContentType: resp.Header.Get("Content-Type"), + ETag: resp.Header.Get("ETag"), + Body: body, + } + + // Send the result back via the channel + resultsChan <- result{IconURL: iconURL, CachedImage: cachedImage} +} + +// resolveIconURL processes the iconURL to make it an absolute URL if it is relative +func resolveIconURL(iconURL string, baseURL *url.URL) string { + if strings.HasPrefix(iconURL, ".") { + // Relative URL starting with "." + // Resolve the relative URL based on the base URL + return baseURL.Scheme + "://" + baseURL.Host + iconURL[1:] + } else if strings.HasPrefix(iconURL, "/") { + // Relative URL starting with "/" + // Append the relative URL to the base URL + return baseURL.Scheme + "://" + baseURL.Host + iconURL + } else { + // Relative URL without starting dot or slash + // Construct the absolute URL based on the current page's URL path + baseURLPath := path.Dir(baseURL.Path) + if baseURLPath == "." { + baseURLPath = "" + } + return baseURL.Scheme + "://" + baseURL.Host + baseURLPath + "/" + iconURL + } +} + + func PingURL(w http.ResponseWriter, req *http.Request) { if utils.LoggedInOnly(w, req) != nil { return diff --git a/src/metrics/index.go b/src/metrics/index.go index 0d6c5b2..23b6f9f 100644 --- a/src/metrics/index.go +++ b/src/metrics/index.go @@ -115,6 +115,12 @@ func SaveMetrics() { }, } + CheckAlerts(dp.Key, "latest", utils.AlertMetricTrack{ + Key: dp.Key, + Object: dp.Object, + Max: dp.Max, + }, dp.Value) + // This ensures that if the document doesn't exist, it'll be created options := options.Update().SetUpsert(true) @@ -188,12 +194,6 @@ func PushSetMetric(key string, value int, def DataDef) { } } - CheckAlerts(key, "latest", utils.AlertMetricTrack{ - Key: key, - Object: def.Object, - Max: def.Max, - }, value) - lastInserted[key] = originalValue }() }