[release] v0.12.0-unstable47

This commit is contained in:
Yann Stepienik 2023-11-07 16:26:51 +00:00
parent 2daf467650
commit 9ec6784b26
27 changed files with 653 additions and 91 deletions

View file

@ -1,27 +1,31 @@
## Version 0.12.0
- New Dashboard
- New metrics gathering system
- New alerts system
- New notification center
- New events manager
- Integrated a new docker-less mode of functioning for networking
- Added Button to force reset HTTPS cert in settings
- New color slider with reset buttons
- New real time persisting andd optimized metrics monitoring system (RAM, CPU, Network, disk, requests, errors, etc...)
- New Dashboard with graphs for metrics, including graphs in many screens such as home, routes and servapps
- New customizable alerts system based on metrics in real time, with included preset for anti-crypto mining and anti memory leak
- New events manager (improved logs with requests and advanced search)
- New notification system
- Added Marketplace UI to edit sources, with new display of 3rd party sources
- Added a notification when updating a container, renewing certs, etc...
- Certificates now renew sooner to avoid Let's Encrypt sending emails about expiring certificates
- Added option to disable routes without deleting them
- Improved icon loading speed, and added proper placeholder
- Added lazyloading to URL and Servapp pages images
- Marketplace now fetch faster (removed the domain indirection to directly fetch from github)
- Integrated a new docker-less mode of functioning for networking
- 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 Button to force reset HTTPS cert in settings
- Added lazyloading to URL and Servapp pages images
- Fixed annoying marketplace screenshot bug (you know what I'm talking about!)
- New color slider with reset buttons
- Redirect static folder to host if possible
- New Homescreen look
- Added option to disable routes without deleting them
- Fixed blinking modals issues
- Improve display or icons [fixes #121]
- Refactored Mongo connection code [fixes #111]
- Forward simultaneously TCP and UDP [fixes #122]
## Version 0.11.3
- Fix missing even subscriber on export
- Fix missing event subscriber on export
## Version 0.11.2
- Improve Docker exports logs

View file

@ -47,7 +47,7 @@ export const CosmosInputText = ({ name, style, value, errors, multiline, type, p
<OutlinedInput
id={name}
type={type ? type : 'text'}
value={value || (formik && formik.values[name])}
value={value || (formik && getNestedValue(formik.values, name))}
name={name}
multiline={multiline}
onBlur={(...ar) => {
@ -101,7 +101,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
<OutlinedInput
id={name}
type={showPassword ? 'text' : 'password'}
value={formik.values[name]}
value={getNestedValue(formik.values, name)}
name={name}
autoComplete={autoComplete}
onBlur={formik.handleBlur}

View file

@ -1,4 +1,4 @@
import { Box, CircularProgress, Input, InputAdornment, Stack } from "@mui/material";
import { Box, CircularProgress, Input, InputAdornment, Stack, Tooltip } from "@mui/material";
import { HomeBackground, TransparentHeader } from "../home";
import { useEffect, useState } from "react";
import * as API from "../../api";
@ -10,18 +10,42 @@ import { Paper, Button, Chip } from '@mui/material'
import { Link } from "react-router-dom";
import { Link as LinkMUI } from '@mui/material'
import DockerComposeImport from '../servapps/containers/docker-compose';
import { AppstoreAddOutlined, SearchOutlined } from "@ant-design/icons";
import { AppstoreAddOutlined, SearchOutlined, WarningOutlined } from "@ant-design/icons";
import ResponsiveButton from "../../components/responseiveButton";
import { useClientInfos } from "../../utils/hooks";
import EditSourcesModal from "./sources";
function Screenshots({ screenshots }) {
const aspectRatioContainerStyle = {
position: 'relative',
overflow: 'hidden',
paddingTop: '56.25%', // 9 / 16 = 0.5625 or 56.25%
height: 0,
};
// This will position the image correctly within the aspect ratio container
const imageStyle = {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
width: '100%',
height: '100%',
objectFit: 'cover', // This will cover the area without losing the aspect ratio
};
return screenshots.length > 1 ? (
<Carousel animation="slide" navButtonsAlwaysVisible={false} fullHeightHover="true" swipe={false}>
{
screenshots.map((item, i) => <img style={{ maxHeight: '300px', height: '100%', maxWidth: '100%' }} key={i} src={item} />)
screenshots.map((item, i) => <div style={{height: "400px"}}>
<img style={{ maxHeight: '100%', width: '100%' }} key={i} src={item} />
</div>)
}
</Carousel>)
: <img src={screenshots[0]} style={{ maxHeight: '300px', height: '100%', maxWidth: '100%' }} />
: <div style={{height: "400px"}}>
<img src={screenshots[0]} style={{ maxHeight: '100%', width: '100%' }} />
</div>
}
function Showcases({ showcase, isDark, isAdmin }) {
@ -122,18 +146,44 @@ const MarketPage = () => {
// borderTop: '1px solid rgb(220,220,220)'
};
useEffect(() => {
const refresh = () => {
API.market.list().then((res) => {
setApps(res.data.all);
setShowcase(res.data.showcase);
});
};
useEffect(() => {
refresh();
}, []);
let openedApp = null;
if (appName && Object.keys(apps).length > 0) {
openedApp = apps[appStore].find((app) => app.name === appName);
openedApp.appstore = appStore;
}
let appList = apps && Object.keys(apps).reduce((acc, appstore) => {
const a = apps[appstore].map((app) => {
app.appstore = appstore;
return app;
});
return acc.concat(a);
}, []);
appList.sort((a, b) => {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
return 0;
});
return <>
<HomeBackground />
<TransparentHeader />
@ -186,7 +236,7 @@ const MarketPage = () => {
<Stack direction="row" spacing={2}>
<img src={openedApp.icon} style={{ width: '36px', height: '36px' }} />
<h2>{openedApp.name}</h2>
<h2>{openedApp.name} <span style={{color:'grey'}}>{openedApp.appstore != 'cosmos-cloud' ? (' @ '+openedApp.appstore) : ''}</span></h2>
</Stack>
<div>
@ -197,6 +247,14 @@ const MarketPage = () => {
{openedApp.supported_architectures && openedApp.supported_architectures.slice(0, 8).map((tag) => <Chip label={tag} />)}
</div>
{openedApp.appstore != 'cosmos-cloud' && <div>
<div>
<Tooltip title="This app is not hosted on the Cosmos Cloud App Store. It is not officially verified and tested.">
<WarningOutlined />
</Tooltip> <strong>source:</strong> {openedApp.appstore}
</div>
</div>}
<div>
<div><strong>repository:</strong> <LinkMUI href={openedApp.repository}>{openedApp.repository}</LinkMUI></div>
<div><strong>image:</strong> <LinkMUI href={openedApp.image}>{openedApp.image}</LinkMUI></div>
@ -259,6 +317,7 @@ const MarketPage = () => {
>Start ServApp</ResponsiveButton>
</Link>
<DockerComposeImport refresh={() => { }} />
<EditSourcesModal onSave={refresh} />
</Stack>
{(!apps || !Object.keys(apps).length) && <Box style={{
width: '100%',
@ -275,8 +334,7 @@ const MarketPage = () => {
</Box>}
{apps && Object.keys(apps).length > 0 && <Grid2 container spacing={{ xs: 1, sm: 1, md: 2 }}>
{Object.keys(apps).map(appstore => apps[appstore]
.filter((app) => {
{appList.filter((app) => {
if (!search || search.length <= 2) {
return true;
}
@ -284,17 +342,18 @@ const MarketPage = () => {
app.tags.join(' ').toLowerCase().includes(search.toLowerCase());
})
.map((app) => {
return <Grid2 style={{
return <Grid2
style={{
...gridAnim,
cursor: 'pointer',
}} xs={12} sm={12} md={6} lg={4} xl={3} key={app.name} item><Link to={"/cosmos-ui/market-listing/" + appstore + "/" + app.name} style={{
}} xs={12} sm={12} md={6} lg={4} xl={3} key={app.name + app.appstore} item><Link to={"/cosmos-ui/market-listing/" + app.appstore + "/" + app.name} style={{
textDecoration: 'none',
}}>
<div key={app.name} style={appCardStyle(theme)}>
<Stack spacing={3} direction={'row'} alignItems={'center'} style={{ padding: '0px 15px' }}>
<img src={app.icon} style={{ width: 64, height: 64 }} />
<Stack spacing={1}>
<div style={{ fontWeight: "bold" }}>{app.name}</div>
<div style={{ fontWeight: "bold" }}>{app.name}<span style={{color:'grey'}}>{app.appstore != 'cosmos-cloud' ? (' @ '+app.appstore) : ''}</span></div>
<div style={{
height: '40px',
overflow: 'hidden',
@ -317,7 +376,7 @@ const MarketPage = () => {
</Link>
</Grid2>
}))}
})}
</Grid2>}
</Stack>
</Stack>

View file

@ -0,0 +1,183 @@
import * as React from 'react';
import IsLoggedIn from '../../isLoggedIn';
import * as API from '../../api';
import MainCard from '../../components/MainCard';
import { Formik, Field, useFormik, FormikProvider } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Button,
Checkbox,
FormControlLabel,
Grid,
InputLabel,
OutlinedInput,
Stack,
FormHelperText,
TextField,
MenuItem,
Skeleton,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Box,
} from '@mui/material';
import { ContainerOutlined, ExclamationCircleOutlined, InfoCircleOutlined, PlusCircleOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons';
import PrettyTableView from '../../components/tableView/prettyTableView';
import { DeleteButton } from '../../components/delete';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
import ResponsiveButton from '../../components/responseiveButton';
const AlertValidationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
trackingMetric: Yup.string().required('Tracking metric is required'),
conditionOperator: Yup.string().required('Condition operator is required'),
conditionValue: Yup.number().required('Condition value is required'),
period: Yup.string().required('Period is required'),
});
const EditSourcesModal = ({ onSave }) => {
const [config, setConfig] = React.useState(null);
const [open, setOpen] = React.useState(false);
function getConfig() {
API.config.get().then((res) => {
setConfig(res.data);
});
}
React.useEffect(() => {
getConfig();
}, []);
const formik = useFormik({
initialValues: {
sources: config ? config.MarketConfig.Sources : [],
},
enableReinitialize: true, // This will reinitialize the form when `config` changes
// validationSchema: AlertValidationSchema,
onSubmit: (values) => {
values.sources = values.sources.filter((a) => !a.removed);
// setIsLoading(true);
let toSave = {
...config,
MarketConfig: {
...config.MarketConfig,
Sources: values.sources,
}
};
setOpen(false);
return API.config.set(toSave).then(() => {
onSave();
});
},
validate: (values) => {
const errors = {};
values.sources.forEach((source, index) => {
if (source.Name === '') {
errors[`sources.${index}.Name`] = 'Name is required';
}
if (source.Url === '') {
errors[`sources.${index}.Url`] = 'URL is required';
}
if (source.Name === 'cosmos-cloud' || values.sources.filter((s) => s.Name === source.Name).length > 1) {
errors[`sources.${index}.Name`] = 'Name must be unique';
}
});
return errors;
}
});
return (<>
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Edit Sources</DialogTitle>
{config && <FormikProvider value={formik}>
<form onSubmit={formik.handleSubmit}>
<DialogContent>
<Stack spacing={2}>
{formik.values.sources
.map((action, index) => {
return !action.removed && <>
<Stack spacing={0} key={index}>
<Stack direction="row" spacing={2}>
<CosmosInputText
name={`sources.${index}.Name`}
label="Name"
formik={formik}
/>
<div style={{ flexGrow: 1 }}>
<CosmosInputText
name={`sources.${index}.Url`}
label="URL"
formik={formik}
/>
</div>
<Box style={{
height: '95px',
display: 'flex',
alignItems: 'center',
}}>
<DeleteButton
onDelete={() => {
formik.setFieldValue(`sources.${index}.removed`, true);
}}
/>
</Box>
</Stack>
<div>
<FormHelperText error>{formik.errors[`sources.${index}.Name`]}</FormHelperText>
</div>
<div>
<FormHelperText error>{formik.errors[`sources.${index}.Url`]}</FormHelperText>
</div>
</Stack>
</>
})}
<Button
variant="outlined"
color="primary"
startIcon={<PlusCircleOutlined />}
onClick={() => {
formik.setFieldValue('sources', [
...formik.values.sources,
{
Name: '',
Url: '',
},
]);
}}>
Add Source
</Button>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant='contained' type="submit" disabled={formik.isSubmitting || !formik.isValid}>Save</Button>
</DialogActions>
</form>
</FormikProvider>}
</Dialog>
<ResponsiveButton
variant="outlined"
startIcon={<ContainerOutlined />}
onClick={() => setOpen(true)}
>Sources</ResponsiveButton>
</>
);
};
export default EditSourcesModal;

View file

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

View file

@ -16,8 +16,7 @@
[![DiscordLink](https://img.shields.io/discord/1083875833824944188?label=Discord&logo=Discord&style=flat-square)](https://discord.gg/PwMWwsrwHA) ![CircleCI](https://img.shields.io/circleci/build/github/azukaar/Cosmos-Server?token=6efd010d0f82f97175f04a6acf2dae2bbcc4063c&style=flat-square)
Cosmos is a self-hosted platform for running server applications securely and with built-in privacy features. It acts as a secure gateway to your application, as well as a server manager. It aims to solve the increasingly worrying problem of vulnerable self-hosted applications and personal servers.
☁️ Cosmos is the most secure and easy way to selfhost a Home Server. It acts as a secure gateway to your application, as well as a server manager. It aims to solve the increasingly worrying problem of vulnerable self-hosted applications and personal servers.
<p align="center">
<br/>
@ -37,7 +36,7 @@ Cosmos is a self-hosted platform for running server applications securely and wi
![screenshot1](./screenshot1.png)
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure them all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution torun and secure them all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
Cosmos is a:
@ -155,7 +154,7 @@ in this command, `-v /:/mnt/host` is optional and allow to manage folders from C
`--privileged` is also optional, but it is required if you use hardening software like AppArmor or SELinux, as they restrict access to the docker socket. It is also required for Constellation to work. If you don't want to use it, you can add the following capabilities: NET_ADMIN for Constellation.
Once installed, simply go to `http://your-server-ip` and follow the instructions of the setup wizard.
Once installed, simply go to `http://your-server-ip` and follow the instructions of the setup wizard. **always start the install with the browser in incognito mode** to avoid issues with your browser cache.
Port 4242 is a UDP port used for the Constellation VPN.

View file

@ -123,6 +123,10 @@ func CRON() {
s.Every(1).Day().At("01:00").Do(checkCerts)
s.Every(6).Hours().Do(checkUpdatesAvailable)
s.Every(1).Hours().Do(utils.CleanBannedIPs)
s.Every(1).Day().At("00:00").Do(func() {
utils.CleanupByDate("notifications")
utils.CleanupByDate("events")
})
s.Start()
}()
}

View file

@ -95,6 +95,14 @@ func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
config.HTTPConfig.ProxyConfig.Routes = routes
utils.SetBaseMainConfig(config)
utils.TriggerEvent(
"cosmos.settings",
"Settings updated",
"success",
"",
map[string]interface{}{
})
utils.RestartHTTPServer()

View file

@ -40,6 +40,14 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
request.NewInstall = config.NewInstall
utils.SetBaseMainConfig(request)
utils.TriggerEvent(
"cosmos.settings",
"Settings updated",
"success",
"",
map[string]interface{}{
})
utils.DisconnectDB()
authorizationserver.Init()

View file

@ -147,6 +147,18 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
return
}
utils.TriggerEvent(
"cosmos.constellation.device.create",
"Device created",
"success",
"",
map[string]interface{}{
"deviceName": deviceName,
"nickname": nickname,
"publicKey": key,
"ip": request.IP,
})
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{

View file

@ -966,6 +966,20 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
OnLog("\n")
OnLog(utils.DoSuccess("[OPERATION SUCCEEDED]. SERVICE STARTED\n"))
servicesNames := []string{}
for _, service := range serviceRequest.Services {
servicesNames = append(servicesNames, service.Name)
}
utils.TriggerEvent(
"cosmos.docker.compose.create",
"Service created",
"success",
"",
map[string]interface{}{
"services": servicesNames,
})
return nil
}

View file

@ -49,6 +49,16 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
return
}
utils.TriggerEvent(
"cosmos.docker.isolate",
"Container network isolation changed",
"success",
"container@"+containerName,
map[string]interface{}{
"container": containerName,
"status": status,
})
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})

View file

@ -101,6 +101,15 @@ func RecreateContainer(containerID string, containerConfig types.ContainerJSON)
} else {
return EditContainer(containerID, containerConfig, false)
}
utils.TriggerEvent(
"cosmos.docker.recreate",
"Cosmos Container Recreate",
"success",
"container@" + containerID,
map[string]interface{}{
"container": containerID,
})
return "", nil
}

View file

@ -60,41 +60,43 @@ func DockerListenEvents() error {
onNetworkConnect(msg.Actor.ID)
}
level := "info"
if msg.Type == "image" {
level = "debug"
if msg.Action != "exec_create" && msg.Action != "exec_start" && msg.Action != "exec_die" {
level := "info"
if msg.Type == "image" {
level = "debug"
}
if msg.Action == "destroy" || msg.Action == "delete" || msg.Action == "kill" || msg.Action == "die" {
level = "warning"
}
if msg.Action == "create" || msg.Action == "start" {
level = "success"
}
object := ""
if msg.Type == "container" {
object = "container@" + msg.Actor.Attributes["name"]
} else if msg.Type == "network" {
object = "network@" + msg.Actor.Attributes["name"]
} else if msg.Type == "image" {
object = "image@" + msg.Actor.Attributes["name"]
} else if msg.Type == "volume" && msg.Actor.Attributes["name"] != "" {
object = "volume@" + msg.Actor.Attributes["name"]
}
utils.TriggerEvent(
"cosmos.docker.event." + msg.Type + "." + msg.Action,
"Docker Event " + msg.Type + " " + msg.Action,
level,
object,
map[string]interface{}{
"type": msg.Type,
"action": msg.Action,
"actor": msg.Actor,
"status": msg.Status,
"from": msg.From,
"scope": msg.Scope,
})
}
if msg.Action == "destroy" || msg.Action == "delete" || msg.Action == "kill" || msg.Action == "die" {
level = "warning"
}
if msg.Action == "create" || msg.Action == "start" {
level = "success"
}
object := ""
if msg.Type == "container" {
object = "container@" + msg.Actor.Attributes["name"]
} else if msg.Type == "network" {
object = "network@" + msg.Actor.Attributes["name"]
} else if msg.Type == "image" {
object = "image@" + msg.Actor.Attributes["name"]
} else if msg.Type == "volume" && msg.Actor.Attributes["name"] != "" {
object = "volume@" + msg.Actor.Attributes["name"]
}
utils.TriggerEvent(
"cosmos.docker.event." + msg.Type + "." + msg.Action,
"Docker Event " + msg.Type + " " + msg.Action,
level,
object,
map[string]interface{}{
"type": msg.Type,
"action": msg.Action,
"actor": msg.Actor,
"status": msg.Status,
"from": msg.From,
"scope": msg.Scope,
})
}
}
}()

View file

@ -147,7 +147,7 @@ func tokenMiddleware(next http.Handler) http.Handler {
})
}
func SecureAPI(userRouter *mux.Router, public bool) {
func SecureAPI(userRouter *mux.Router, public bool, publicCors bool) {
if(!public) {
userRouter.Use(tokenMiddleware)
}
@ -162,9 +162,13 @@ func SecureAPI(userRouter *mux.Router, public bool) {
},
},
))
if(publicCors || public) {
userRouter.Use(utils.PublicCORS)
}
userRouter.Use(utils.MiddlewareTimeout(45 * time.Second))
userRouter.Use(proxy.BotDetectionMiddleware)
userRouter.Use(httprate.Limit(120, 1*time.Minute,
userRouter.Use(httprate.Limit(180, 1*time.Minute,
httprate.WithKeyFuncs(httprate.KeyByIP),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
utils.Error("Too many requests. Throttling", nil)
@ -334,7 +338,7 @@ func InitServer() *mux.Router {
}
logoAPI := router.PathPrefix("/logo").Subrouter()
SecureAPI(logoAPI, true)
SecureAPI(logoAPI, true, true)
logoAPI.HandleFunc("/", SendLogo)
@ -413,7 +417,7 @@ func InitServer() *mux.Router {
srapi.Use(utils.EnsureHostname)
}
SecureAPI(srapi, false)
SecureAPI(srapi, false, false)
pwd,_ := os.Getwd()
utils.Log("Starting in " + pwd)
@ -437,13 +441,13 @@ func InitServer() *mux.Router {
}))
userRouter := router.PathPrefix("/oauth2").Subrouter()
SecureAPI(userRouter, false)
SecureAPI(userRouter, false, true)
serverRouter := router.PathPrefix("/oauth2").Subrouter()
SecureAPI(serverRouter, true)
SecureAPI(serverRouter, true, true)
wellKnownRouter := router.PathPrefix("/").Subrouter()
SecureAPI(wellKnownRouter, true)
SecureAPI(wellKnownRouter, true, true)
authorizationserver.RegisterHandlers(wellKnownRouter, userRouter, serverRouter)

View file

@ -3,6 +3,7 @@ package market
import (
"net/http"
"encoding/json"
"fmt"
"github.com/azukaar/cosmos-server/src/utils"
)
@ -17,8 +18,23 @@ func MarketGet(w http.ResponseWriter, req *http.Request) {
}
if(req.Method == "GET") {
config := utils.GetMainConfig()
configSourcesList := config.MarketConfig.Sources
configSources := map[string]bool{
"cosmos-cloud": true,
}
for _, source := range configSourcesList {
configSources[source.Name] = true
}
utils.Debug(fmt.Sprintf("MarketGet: Config sources: %v", configSources))
Init()
err := updateCache(w, req)
if err != nil {
utils.Error("MarketGet: Error while updating cache", err)
utils.HTTPError(w, "Error while updating cache", http.StatusInternalServerError, "MK002")
return
}
@ -28,19 +44,23 @@ func MarketGet(w http.ResponseWriter, req *http.Request) {
}
for _, market := range currentMarketcache {
if !configSources[market.Name] {
continue
}
utils.Debug(fmt.Sprintf("MarketGet: Adding market %v", market.Name))
results := []appDefinition{}
for _, app := range market.Results.All {
// if i < 10 {
results = append(results, app)
// } else {
// break
// }
results = append(results, app)
}
marketGetResult.All[market.Name] = results
}
if len(currentMarketcache) > 0 {
marketGetResult.Showcase = currentMarketcache[0].Results.Showcase
for _, market := range currentMarketcache {
if market.Name == "cosmos-cloud" {
marketGetResult.Showcase = market.Results.Showcase
}
}
}
json.NewEncoder(w).Encode(map[string]interface{}{

View file

@ -6,23 +6,59 @@ import (
func Init() {
config := utils.GetMainConfig()
currentMarketcache = []marketCacheObject{}
sources := config.MarketConfig.Sources
inConfig := map[string]bool{
"cosmos-cloud": true,
}
for _, source := range sources {
inConfig[source.Name] = true
}
if currentMarketcache == nil {
currentMarketcache = []marketCacheObject{}
}
inCache := map[string]bool{}
toRemove := []string{}
for _, cachedMarket := range currentMarketcache {
inCache[cachedMarket.Name] = true
if !inConfig[cachedMarket.Name] {
utils.Log("MarketInit: Removing market " + cachedMarket.Name)
toRemove = append(toRemove, cachedMarket.Name)
}
}
// remove markets that are not in config
for _, name := range toRemove {
for index, cachedMarket := range currentMarketcache {
if cachedMarket.Name == name {
currentMarketcache = append(currentMarketcache[:index], currentMarketcache[index+1:]...)
break
}
}
}
// prepend the default market
defaultMarket := utils.MarketSource{
Url: "https://cosmos-cloud.io/repository",
Url: "https://azukaar.github.io/cosmos-servapps-official/index.json",
Name: "cosmos-cloud",
}
sources = append([]utils.MarketSource{defaultMarket}, sources...)
for _, marketDef := range sources {
market := marketCacheObject{
Url: marketDef.Url,
Name: marketDef.Name,
}
currentMarketcache = append(currentMarketcache, market)
// add markets that are in config but not in cache
if !inCache[marketDef.Name] {
market := marketCacheObject{
Url: marketDef.Url,
Name: marketDef.Name,
}
utils.Log("MarketInit: Added market " + market.Name)
currentMarketcache = append(currentMarketcache, market)
utils.Log("MarketInit: Added market " + market.Name)
}
}
}

View file

@ -61,9 +61,24 @@ func updateCache(w http.ResponseWriter, req *http.Request) error {
continue
}
result.Source = cachedMarket.Url
if cachedMarket.Name != "cosmos-cloud" {
result.Showcase = []appDefinition{}
}
cachedMarket.Results = result
cachedMarket.LastUpdate = time.Now()
utils.TriggerEvent(
"cosmos.market.update",
"Market updated",
"success",
"",
map[string]interface{}{
"market": cachedMarket.Name,
"numberOfApps": len(result.All),
})
utils.Log("MarketUpdate: Updated market " + result.Source + " with " + string(len(result.All)) + " results")
// save to cache

View file

@ -38,11 +38,11 @@ type smartShieldState struct {
}
type userUsedBudget struct {
ClientID string
Time float64
Requests int
Bytes int64
Simultaneous int
ClientID string `json:"clientID"`
Time float64 `json:"time"`
Requests int `json:"requests"`
Bytes int64 `json:"bytes"`
Simultaneous int `json:"simultaneous"`
}
var shield smartShieldState
@ -379,6 +379,20 @@ func SmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) func(h
lastBan := shield.GetLastBan(policy, userConsumed)
go metrics.PushShieldMetrics("smart-shield")
utils.IncrementIPAbuseCounter(clientID)
utils.TriggerEvent(
"cosmos.proxy.shield.abuse." + route.Name,
"Proxy Shield " + route.Name + " Abuse by " + clientID,
"warning",
"route@" + route.Name,
map[string]interface{}{
"route": route.Name,
"consumed": userConsumed,
"lastBan": lastBan,
"clientID": clientID,
"url": r.URL,
})
utils.Log("SmartShield: User is blocked due to abuse: " + fmt.Sprintf("%+v", lastBan))
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return

View file

@ -77,6 +77,15 @@ func UserCreate(w http.ResponseWriter, req *http.Request) {
return
}
utils.TriggerEvent(
"cosmos.user.create",
"User created",
"success",
"",
map[string]interface{}{
"nickname": nickname,
})
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{

View file

@ -86,6 +86,15 @@ func ResetPassword(w http.ResponseWriter, req *http.Request) {
return
}
utils.TriggerEvent(
"cosmos.user.passwordreset",
"Password reset sent",
"success",
"",
map[string]interface{}{
"nickname": user.Nickname,
})
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})

View file

@ -102,6 +102,15 @@ func UserRegister(w http.ResponseWriter, req *http.Request) {
}
}
utils.TriggerEvent(
"cosmos.user.register",
"User registered",
"success",
"",
map[string]interface{}{
"nickname": nickname,
})
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})

40
src/utils/cleanup.go Normal file
View file

@ -0,0 +1,40 @@
package utils
import (
"time"
"strconv"
"context"
"go.mongodb.org/mongo-driver/bson"
)
type CleanupObject struct {
Date time.Time
}
func CleanupByDate(collectionName string) {
c, errCo := GetCollection(GetRootAppId(), collectionName)
if errCo != nil {
MajorError("Database Cleanup", errCo)
return
}
del, err := c.DeleteMany(context.Background(), bson.M{"Date": bson.M{"$lt": time.Now().AddDate(0, -1, 0)}})
if err != nil {
MajorError("Database Cleanup", err)
return
}
Log("Cleanup: " + collectionName + " " + strconv.Itoa(int(del.DeletedCount)) + " objects deleted")
TriggerEvent(
"cosmos.database.cleanup",
"Database Cleanup of " + collectionName,
"success",
"",
map[string]interface{}{
"collection": collectionName,
"deleted": del.DeletedCount,
})
}

View file

@ -147,5 +147,15 @@ func SendEmail(recipients []string, subject string, body string) error {
ServerURL,
))
TriggerEvent(
"cosmos.email.send",
"Email sent",
"success",
"",
map[string]interface{}{
"recipients": recipients,
"subject": subject,
})
return send(hostPort, auth, config.EmailConfig.From, recipients, msg)
}

View file

@ -57,6 +57,16 @@ func MajorError(message string, err error) {
log.Println(Red + "[ERROR] " + message + " : " + errStr + Reset)
}
TriggerEvent(
"cosmos.error",
"Critical Error",
"error",
"",
map[string]interface{}{
"message": message,
"error": errStr,
})
WriteNotification(Notification{
Recipient: "admin",
Title: "Server Error",

View file

@ -163,6 +163,15 @@ func CORSHeader(origin string) func(next http.Handler) http.Handler {
}
}
func PublicCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
next.ServeHTTP(w, r)
})
}
func AcceptHeader(accept string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -218,6 +227,18 @@ func BlockByCountryMiddleware(blockedCountries []string, CountryBlacklistIsWhite
if blocked {
PushShieldMetrics("geo")
IncrementIPAbuseCounter(ip)
TriggerEvent(
"cosmos.proxy.shield.geo",
"Proxy Shield Geo blocked",
"warning",
"",
map[string]interface{}{
"clientID": ip,
"country": countryCode,
"url": r.URL.String(),
})
http.Error(w, "Access denied", http.StatusForbidden)
return
}
@ -253,6 +274,16 @@ func BlockPostWithoutReferer(next http.Handler) http.Handler {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if ip != "" {
TriggerEvent(
"cosmos.proxy.shield.referer",
"Proxy Shield Referer blocked",
"warning",
"",
map[string]interface{}{
"clientID": ip,
"url": r.URL.String(),
})
IncrementIPAbuseCounter(ip)
}
@ -295,6 +326,16 @@ func EnsureHostname(next http.Handler) http.Handler {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
if ip != "" {
TriggerEvent(
"cosmos.proxy.shield.hostname",
"Proxy Shield hostname blocked",
"warning",
"",
map[string]interface{}{
"clientID": ip,
"hostname": r.Host,
"url": r.URL.String(),
})
IncrementIPAbuseCounter(ip)
}
@ -389,6 +430,17 @@ func Restrictions(RestrictToConstellation bool, WhitelistInboundIPs []string) fu
if(!isInConstellation) {
if(!isUsingWhiteList) {
PushShieldMetrics("ip-whitelists")
TriggerEvent(
"cosmos.proxy.shield.whitelist",
"Proxy Shield IP blocked by whitelist",
"warning",
"",
map[string]interface{}{
"clientID": ip,
"url": r.URL.String(),
})
IncrementIPAbuseCounter(ip)
Error("Request from " + ip + " is blocked because of restrictions", nil)
Debug("Blocked by RestrictToConstellation isInConstellation isUsingWhiteList")
@ -396,6 +448,17 @@ func Restrictions(RestrictToConstellation bool, WhitelistInboundIPs []string) fu
return
} else if (!isInWhitelist) {
PushShieldMetrics("ip-whitelists")
TriggerEvent(
"cosmos.proxy.shield.whitelist",
"Proxy Shield IP blocked by whitelist",
"warning",
"",
map[string]interface{}{
"clientID": ip,
"url": r.URL.String(),
})
IncrementIPAbuseCounter(ip)
Error("Request from " + ip + " is blocked because of restrictions", nil)
Debug("Blocked by RestrictToConstellation isInConstellation isInWhitelist")
@ -405,6 +468,17 @@ func Restrictions(RestrictToConstellation bool, WhitelistInboundIPs []string) fu
}
} else if(isUsingWhiteList && !isInWhitelist) {
PushShieldMetrics("ip-whitelists")
TriggerEvent(
"cosmos.proxy.shield.whitelist",
"Proxy Shield IP blocked by whitelist",
"warning",
"",
map[string]interface{}{
"clientID": ip,
"url": r.URL.String(),
})
IncrementIPAbuseCounter(ip)
Error("Request from " + ip + " is blocked because of restrictions", nil)
Debug("Blocked by RestrictToConstellation isInConstellation isUsingWhiteList isInWhitelist")