Browse Source

[release] v0.12.0-unstable42

Yann Stepienik 1 year ago
parent
commit
34bc76dbf8

+ 1 - 0
changelog.md

@@ -8,6 +8,7 @@
  - Added Button to force reset HTTPS cert in settings
  - Added Button to force reset HTTPS cert in settings
  - New color slider with reset buttons
  - New color slider with reset buttons
  - Added a notification when updating a container
  - Added a notification when updating a container
+ - Improved icon loading speed, and added proper placeholder
  - Added lazyloading to URL and Servapp pages images
  - 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 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
  - Added a button in the servapp page to easily download the docker backup

+ 48 - 0
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 (
+    <>
+      <img
+        ref={setImageRef}
+        {...props}
+        src={imageSrc}
+        alt={alt}
+        onLoad={onLoad}
+        onError={onError}
+        // style={{ opacity: imageSrc === src ? 1 : 0, transition: 'opacity 0.5s ease-in-out' }}
+      />
+      {/* This image will load the actual image and then handleImageLoad will be triggered */}
+      <img
+        {...props}
+        src={src}
+        alt={alt}
+        style={{ display: 'none' }} // Hide this image
+        onLoad={handleImageLoad}
+        onError={onError}
+      />
+    </>
+  );
+}
+
+export default ImageWithPlaceholder;

+ 8 - 3
client/src/layout/MainLayout/Header/HeaderContent/Notification.jsx

@@ -64,18 +64,22 @@ const Notification = () => {
     const setAsRead = () => {
     const setAsRead = () => {
         let unread = [];
         let unread = [];
         
         
-        let newN = notifications.map((notif) => {
+       notifications.forEach((notif) => {
             if (!notif.Read) {
             if (!notif.Read) {
                 unread.push(notif.ID);
                 unread.push(notif.ID);
             }
             }
-            notif.Read = true;
-            return notif;
         })
         })
 
 
         if (unread.length > 0) {
         if (unread.length > 0) {
             API.users.readNotifs(unread);
             API.users.readNotifs(unread);
         }
         }
+    }
 
 
+    const setLocalAsRead = (id) => {
+        let newN = notifications.map((notif) => {
+            notif.Read = true;
+            return notif;
+        })
         setNotifications(newN);
         setNotifications(newN);
     }
     }
 
 
@@ -104,6 +108,7 @@ const Notification = () => {
         if (anchorRef.current && anchorRef.current.contains(event.target)) {
         if (anchorRef.current && anchorRef.current.contains(event.target)) {
             return;
             return;
         }
         }
+        setLocalAsRead();
         setOpen(false);
         setOpen(false);
     };
     };
 
 

+ 2 - 1
client/src/pages/config/routes/routeoverview.jsx

@@ -12,6 +12,7 @@ import { redirectToLocal } from '../../../utils/indexs';
 import { CosmosCheckbox } from '../users/formShortcuts';
 import { CosmosCheckbox } from '../users/formShortcuts';
 import { Field } from 'formik';
 import { Field } from 'formik';
 import MiniPlotComponent from '../../dashboard/components/mini-plot';
 import MiniPlotComponent from '../../dashboard/components/mini-plot';
+import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
 
 
 const info = {
 const info = {
   backgroundColor: 'rgba(0, 0, 0, 0.1)',
   backgroundColor: 'rgba(0, 0, 0, 0.1)',
@@ -39,7 +40,7 @@ const RouteOverview = ({ routeConfig }) => {
       </div>}>
       </div>}>
         <Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
         <Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
           <div>
           <div>
-            <img className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
+            <ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
           </div>
           </div>
           <Stack spacing={2} style={{ width: '100%' }}>
           <Stack spacing={2} style={{ width: '100%' }}>
             <strong><ContainerOutlined />Description</strong>
             <strong><ContainerOutlined />Description</strong>

+ 2 - 1
client/src/pages/config/users/proxyman.jsx

@@ -41,6 +41,7 @@ import { useNavigate } from 'react-router';
 import NewRouteCreate from '../routes/newRoute';
 import NewRouteCreate from '../routes/newRoute';
 import LazyLoad from 'react-lazyload';
 import LazyLoad from 'react-lazyload';
 import MiniPlotComponent from '../../dashboard/components/mini-plot';
 import MiniPlotComponent from '../../dashboard/components/mini-plot';
+import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
 
 
 const stickyButton = {
 const stickyButton = {
   position: 'fixed',
   position: 'fixed',
@@ -166,7 +167,7 @@ const ProxyManagement = () => {
           { 
           { 
             title: '', 
             title: '', 
             field: (r) => <LazyLoad width={"64px"} height={"64px"}>
             field: (r) => <LazyLoad width={"64px"} height={"64px"}>
-              <img className="loading-image" alt="" src={getFaviconURL(r)} width="64px" height="64px"/>
+              <ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(r)} width="64px" height="64px"/>
             </LazyLoad>,
             </LazyLoad>,
             style: {
             style: {
               textAlign: 'center',
               textAlign: 'center',

+ 2 - 2
client/src/pages/dashboard/components/mini-plot.jsx

@@ -39,7 +39,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
   const [ref, inView] = useInView();
   const [ref, inView] = useInView();
 
 
   useEffect(() => {
   useEffect(() => {
-    if(!inView) return;
+    if(!inView || series.length) return;
     
     
     let xAxis = [];
     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)'}}
                 alignItems='center' sx={{padding: '0px 20px', width: '100%', backgroundColor: noBackground ? '' : 'rgba(0,0,0,0.075)'}}
                 justifyContent={'space-around'}>
                 justifyContent={'space-around'}>
 
 
-<Stack direction='column' justifyContent={'center'} alignItems={'flex-start'} spacing={0} style={{
+        <Stack direction='column' justifyContent={'center'} alignItems={'flex-start'} spacing={0} style={{
           width: '160px',
           width: '160px',
           whiteSpace: 'nowrap',
           whiteSpace: 'nowrap',
         }}>
         }}>

+ 4 - 3
client/src/utils/servapp-icon.jsx

@@ -1,12 +1,13 @@
 import { getFaviconURL } from "./routes";
 import { getFaviconURL } from "./routes";
 import logogray from '../assets/images/icons/cosmos_gray.png';
 import logogray from '../assets/images/icons/cosmos_gray.png';
 import LazyLoad from 'react-lazyload';
 import LazyLoad from 'react-lazyload';
+import ImageWithPlaceholder from "../components/imageWithPlaceholder";
 
 
 export const ServAppIcon = ({route, container, width, ...pprops}) => {
 export const ServAppIcon = ({route, container, width, ...pprops}) => {
   return <LazyLoad width={width} height={width}>
   return <LazyLoad width={width} height={width}>
     {(container && container.Labels["cosmos-icon"]) ? 
     {(container && container.Labels["cosmos-icon"]) ? 
-      <img src={container.Labels["cosmos-icon"]} {...pprops} width={width} height={width}></img> :(
-        route ? <img src={getFaviconURL(route)} {...pprops} width={width} height={width}></img>
-          : <img src={logogray} {...pprops} width={width} height={width}></img>)}
+      <ImageWithPlaceholder src={container.Labels["cosmos-icon"]} {...pprops} width={width} height={width}></ImageWithPlaceholder> :(
+        route ? <ImageWithPlaceholder src={getFaviconURL(route)} {...pprops} width={width} height={width}></ImageWithPlaceholder>
+          : <ImageWithPlaceholder src={logogray} {...pprops} width={width} height={width}></ImageWithPlaceholder>)}
   </LazyLoad>;
   </LazyLoad>;
 };
 };

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "cosmos-server",
   "name": "cosmos-server",
-  "version": "0.12.0-unstable41",
+  "version": "0.12.0-unstable42",
   "description": "",
   "description": "",
   "main": "test-server.js",
   "main": "test-server.js",
   "bugs": {
   "bugs": {

+ 5 - 2
src/httpServer.go

@@ -284,8 +284,11 @@ func InitServer() *mux.Router {
 		router.Use(utils.BlockByCountryMiddleware(config.BlockedCountries, config.CountryBlacklistIsWhitelist))
 		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 := router.PathPrefix("/cosmos").Subrouter()
 
 
 	srapi.HandleFunc("/api/dns", GetDNSRoute)
 	srapi.HandleFunc("/api/dns", GetDNSRoute)

+ 150 - 118
src/icons.go

@@ -11,6 +11,7 @@ import (
 	"path"
 	"path"
 	"time"
 	"time"
 	"context"
 	"context"
+	"sync"
 
 
 	"go.deanishe.net/favicon"
 	"go.deanishe.net/favicon"
 
 
@@ -69,154 +70,185 @@ func sendFallback(w http.ResponseWriter) {
 }
 }
 
 
 var IconCacheLock = make(chan bool, 1)
 var IconCacheLock = make(chan bool, 1)
-
+type result struct {
+	IconURL     string
+	CachedImage CachedImage
+	Error       error
+}
 func GetFavicon(w http.ResponseWriter, req *http.Request) {
 func GetFavicon(w http.ResponseWriter, req *http.Request) {
 	if utils.LoggedInOnly(w, req) != nil {
 	if utils.LoggedInOnly(w, req) != nil {
-		return
+			return
 	}
 	}
 
 
 	// get url from query string
 	// get url from query string
 	escsiteurl := req.URL.Query().Get("q")
 	escsiteurl := req.URL.Query().Get("q")
-	
+
 	IconCacheLock <- true
 	IconCacheLock <- true
 	defer func() { <-IconCacheLock }()
 	defer func() { <-IconCacheLock }()
-	
+
 	// URL decode
 	// URL decode
 	siteurl, err := url.QueryUnescape(escsiteurl)
 	siteurl, err := url.QueryUnescape(escsiteurl)
 	if err != nil {
 	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)
-
-		// 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
-		}
+	if req.Method == "GET" {
+			utils.Log("Fetch favicon for " + siteurl)
 
 
-		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},
-		}
+			// Check if we have the favicon in cache
+			if resp, ok := IconCache[siteurl]; ok {
+					utils.Debug("Favicon in cache")
+					sendImage(w, resp)
+					return
+			}
 
 
-		// follow siteurl and check if any redirect. 
-		
-		respNew, err := httpGetWithTimeout(siteurl)
+			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},
+			}
 
 
-		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...)
+			// follow siteurl and check if any redirect.
+			respNew, err := httpGetWithTimeout(siteurl)
+			if err != nil {
+					utils.Error("FaviconFetch", err)
+					icons = append(icons, defaultIcons...)
 			} else {
 			} 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)
-						}
-				}
-			}
-		}
+					siteurl = respNew.Request.URL.String()
+					icons, err = favicon.Find(siteurl)
 
 
-		for _, icon := range icons {
-			if icon.Width <= 256 {
-
-				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
+					if err != nil || len(icons) == 0 {
+							icons = append(icons, defaultIcons...)
 					} else {
 					} 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
-				}
-
-				// 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,
+			}
+
+			// 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
+
+			// 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])
 					sendImage(w, IconCache[siteurl])
 					return
 					return
-				}
 			}
 			}
-	} 
-	utils.Log("Favicon final fallback")
-	sendFallback(w)
-	return
-	
+
+			utils.Log("Favicon final fallback")
+			sendFallback(w)
+			return
+
 	} else {
 	} 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) {
 func PingURL(w http.ResponseWriter, req *http.Request) {
 	if utils.LoggedInOnly(w, req) != nil {
 	if utils.LoggedInOnly(w, req) != nil {
 		return
 		return

+ 6 - 6
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
 			// This ensures that if the document doesn't exist, it'll be created
 			options := options.Update().SetUpsert(true)
 			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
 		lastInserted[key] = originalValue
 	}()
 	}()
 }
 }