[release] v0.12.0-unstable42

This commit is contained in:
Yann Stepienik 2023-11-05 20:34:17 +00:00
parent aa963bb89f
commit 34bc76dbf8
11 changed files with 228 additions and 136 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);
};

View file

@ -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 }) => {
</div>}>
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
<div>
<img className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
</div>
<Stack spacing={2} style={{ width: '100%' }}>
<strong><ContainerOutlined />Description</strong>

View file

@ -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) => <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>,
style: {
textAlign: 'center',

View file

@ -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'}>
<Stack direction='column' justifyContent={'center'} alignItems={'flex-start'} spacing={0} style={{
<Stack direction='column' justifyContent={'center'} alignItems={'flex-start'} spacing={0} style={{
width: '160px',
whiteSpace: 'nowrap',
}}>

View file

@ -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 <LazyLoad width={width} height={width}>
{(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>;
};

View file

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

View file

@ -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)

View file

@ -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

View file

@ -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
}()
}