[skip ci] you know it

This commit is contained in:
Yann Stepienik
2026-02-10 17:10:09 +00:00
parent 2f75fa5484
commit eb992ad21c
37 changed files with 428 additions and 102 deletions
+5 -1
View File
@@ -1,7 +1,11 @@
## Version 0.21.0
- UI refresh for most pages
- Rclone rework, no more sub processes (increased performance and reliability)
- Reworked mount managemenet logic for flexibility
- Added "skip clean URL" option for apps that have invalid URLs like Synology
- Support for tempFS
- Improve support for 0.0.0.0 routes
- Reduce SmartShield false positive on the server panel by having two level of strictness (UI/panel vs. login)
- Rclone rework, no more sub processes (increased performance and reliability)
- Reworked Constellation cluster synchronisation completely. Now there are no more "Master" server and each server is equally capable.
- Any server can be used as DNS (add redundancy too)
- Any server can tunnel another server's URL in any direction
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cosmos</title>
<link rel="icon" type="image/x-icon" href="/src/assets/images/icons/cosmos.png">
<link rel="icon" type="image/x-icon" href="/src/assets/images/icons/logo2.png">
<style>
@media (prefers-color-scheme: dark) {
html {
-1
View File
@@ -5,7 +5,6 @@ import ThemeCustomization from './themes';
import ScrollTop from './components/ScrollTop';
import Snackbar from '@mui/material/Snackbar';
import {Alert, Box} from '@mui/material';
import logo from './assets/images/icons/cosmos.png';
import * as API from './api';
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+9 -1
View File
@@ -31,7 +31,15 @@ const Logo = () => {
: "brightness(0)" // black
}}
/>
<span style={{fontWeight: 'bold', fontSize: '160%', paddingLeft:'10px'}}> Cosmos Cloud</span>
<span style={{fontWeight: 500, fontSize: '160%', paddingLeft:'10px'}}>
Cosmos{' '}
<span style={{
background: `linear-gradient(to right, ${theme.palette.primary.main}, ${theme.palette.secondary.main})`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
}}>Cloud</span>
</span>
</>
);
};
+3 -2
View File
@@ -24,10 +24,11 @@ const HostChip = ({route, settings, style}) => {
return <Chip
label={<><StatusDot status={isOnline == null ? "unknown" : isOnline ? "success" : "error"} size={8} style={{ marginRight: 6 }} />{url}</>}
color="secondary"
color="primary"
variant="outlined"
style={{
paddingRight: '4px',
textDecoration: isOnline ? 'none' : 'underline wavy red',
// textDecoration: isOnline ? 'none' : 'underline wavy red',
...style
}}
onClick={() => {
+7 -7
View File
@@ -9,37 +9,37 @@ let routeImages = {
"TUNNEL": {
label: "Tunnel",
icon: "TU",
backgroundColor: "#082452",
background: "linear-gradient(135deg, #082452, #0a3a7e)",
color: "white",
},
"SERVAPP": {
label: "ServApp",
icon: "SA",
backgroundColor: "#0db7ed",
background: "linear-gradient(135deg, #0db7ed, #0a8abf)",
color: "black",
},
"STATIC": {
label: "Static",
icon: "ST",
backgroundColor: "#f9d71c",
background: "linear-gradient(135deg, #f9d71c, #d4b010)",
color: "black",
},
"REDIRECT": {
label: "Redir",
icon: "RE",
backgroundColor: "#2c3e50",
background: "linear-gradient(135deg, #2c3e50, #1a252f)",
color: "white",
},
"PROXY": {
label: "Proxy",
icon: "PR",
backgroundColor: "#2ecc71",
background: "linear-gradient(135deg, #2ecc71, #1fa85a)",
color: "black",
},
"SPA": {
label: "SPA",
icon: "SP",
backgroundColor: "#e74c3c",
background: "linear-gradient(135deg, #e74c3c, #c0392b)",
color: "black",
},
}
@@ -60,7 +60,7 @@ export const RouteMode = ({route}) => {
icon={<span>{cicon}</span>}
// label={c.label}
sx={{
backgroundColor: c.backgroundColor,
background: c.background,
paddingLeft: "5px",
'& .MuiChip-label': {
@@ -88,6 +88,7 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
HideFromDashboard: routeConfig.HideFromDashboard,
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
RestrictToConstellation: routeConfig.RestrictToConstellation === true,
SkipURLClean: routeConfig.SkipURLClean === true,
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
Tunnel: routeConfig.Tunnel,
TunneledHost: routeConfig.TunneledHost,
@@ -383,6 +384,12 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
label={t('mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderLabel')}
placeholder={t('mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderPlaceholder')}
formik={formik}
/>
<CosmosCheckbox
name="SkipURLClean"
label={t('mgmt.urls.edit.advancedSettings.skipURLCleanCheckbox.skipURLCleanLabel')}
formik={formik}
/></>}
<Alert severity='warning'>
@@ -111,6 +111,9 @@ const ProxyManagement = () => {
API.config.get().then((res) => {
setConfig(res.data);
});
if (!isAdmin) {
return;
}
API.constellation.tunnels().then((res) => {
setTunnels((res.data || []).map(r => {
let route = r.Route;
+1 -1
View File
@@ -164,7 +164,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices, canCreateManage
disabled={!isAdmin}
options={isAdmin ? [
['client', t('mgmt.constellation.setup.deviceType.client')],
['lighthouse', t('mgmt.constellation.setup.deviceType.lighthouse')],
// ['lighthouse', t('mgmt.constellation.setup.deviceType.lighthouse')],
['cosmos-agent', t('mgmt.constellation.setup.deviceType.cosmosAgent'), !canCreateAgent],
['cosmos-manager', t('mgmt.constellation.setup.deviceType.cosmosManager'), !canCreateManager],
] : [
@@ -93,6 +93,9 @@ const NewDockerServiceForm = () => {
spacing={1}
style={{
maxWidth: '1000px',
width: '100%',
margin: 'auto',
marginTop: '20px',
}}
>
<Button
@@ -181,7 +184,7 @@ const NewDockerServiceForm = () => {
title: 'URL',
disabled: maxTab < 1,
children: <Stack spacing={2}>
<MainCard style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
<MainCard style={{ maxWidth: '1000px', width: '100%', margin: 'auto', position: 'relative' }}>
<Checkbox
checked={containerInfo.CreateRoute}
onChange={(e) => {
@@ -304,7 +307,7 @@ const NewDockerServiceForm = () => {
{
title: t('mgmt.servApp.newContainer.reviewStartButton'),
disabled: maxTab < 1,
children: <Stack spacing={2}><NewDockerService service={service} />{nav()}</Stack>
children: <Stack style={{ width: '100%', margin: 'auto', maxWidth: '1000px' }} spacing={2}><NewDockerService service={service} />{nav()}</Stack>
}
]} />}
@@ -57,6 +57,7 @@ const VolumeContainerSetup = ({
};
const formatSource = (mount) => {
if (!mount) return null;
if (mount.startsWith("/")) return mount;
else return "/var/lib/docker/volumes/" + mount + "/_data";
}
@@ -213,6 +214,7 @@ const VolumeContainerSetup = ({
>
<MenuItem value="bind">{t('mgmt.servapps.newContainer.volumes.bindInput')}</MenuItem>
<MenuItem value="volume">{t('global.volume')}</MenuItem>
<MenuItem value="tmpfs">tmpfs</MenuItem>
</TextField>
</div>
),
@@ -248,6 +250,14 @@ const VolumeContainerSetup = ({
onChange={formik.handleChange}
/>
</Stack>
) : r.Type == "tmpfs" ? (
<TextField
className="px-2 my-2"
variant="outlined"
disabled
style={{ minWidth: "200px" }}
value="(memory)"
/>
) : (
<TextField
className="px-2 my-2"
@@ -383,7 +393,7 @@ const VolumeContainerSetup = ({
}}>
{containerInfo && containerInfo.HostConfig && containerInfo.HostConfig.Mounts && <MainCard title={t('mgmt.backup.backups')}>
<Backups pathFilters={
containerInfo.HostConfig.Mounts.map((r) => formatSource(r.Source))
containerInfo.HostConfig.Mounts.map((r) => formatSource(r.Source)).filter(Boolean)
} />
</MainCard>}
</div>}
+2 -1
View File
@@ -381,13 +381,14 @@ const ServApps = ({stack}) => {
return <HostChip route={route} settings/>
})}
{app.networkSettings && app.networkSettings.Networks && app.networkSettings.Networks['host'] &&
<Chip style={{ fontSize: '80%' }} label="Host Network" color="warning" />
<Chip style={{ fontSize: '80%' }} label="Host Network" color="warning" variant='outlined' />
}
{app.ports && app.ports.filter(p => p && p.IP != '::' && p.PublicPort).map((port) => {
return <Chip
style={{ fontSize: '80%', cursor: 'pointer' }}
label={port.PublicPort + ":" + port.PrivatePort}
color="warning"
variant='outlined'
onClick={() => window.open(`${window.location.protocol}//${window.location.hostname}:${port.PublicPort}`, '_blank')}
/>
})}
+1 -1
View File
@@ -1,6 +1,6 @@
// project import
import MainLayout from '../layout/MainLayout';
import logo from '../assets/images/icons/cosmos.png';
import logo from '../assets/images/icons/logo2.png';
import { Navigate } from 'react-router';
import UserManagement from '../pages/config/users/usermanagement';
import ConfigManagement from '../pages/config/users/configman';
+27 -1
View File
@@ -1,6 +1,14 @@
// ==============================|| OVERRIDES - BADGE ||============================== //
export default function Badge(theme) {
const primaryMain = theme.palette.primary.main;
const secondaryMain = theme.palette.secondary.main;
const colorGradient = (name) => {
const c = theme.palette[name];
return `linear-gradient(135deg, ${c.main}, ${c.dark})`;
};
return {
MuiBadge: {
styleOverrides: {
@@ -8,7 +16,25 @@ export default function Badge(theme) {
minWidth: theme.spacing(2),
height: theme.spacing(2),
padding: theme.spacing(0.5)
}
},
colorPrimary: {
backgroundImage: `linear-gradient(135deg, ${primaryMain}, ${secondaryMain})`,
},
colorSecondary: {
backgroundImage: colorGradient('secondary'),
},
colorError: {
backgroundImage: colorGradient('error'),
},
colorSuccess: {
backgroundImage: colorGradient('success'),
},
colorWarning: {
backgroundImage: colorGradient('warning'),
},
colorInfo: {
backgroundImage: colorGradient('info'),
},
}
}
};
+63 -3
View File
@@ -5,13 +5,60 @@ import { alpha } from '@mui/material/styles';
export default function Button(theme) {
const primaryMain = theme.palette.primary.main;
const secondaryMain = theme.palette.secondary.main;
const paperBg = theme.palette.background.paper;
// Two-tone gradient: primary → secondary
const primaryGradient = `linear-gradient(135deg, ${primaryMain}, ${secondaryMain})`;
// Per-color gradient: main → dark
const colorGradient = (name) => {
const c = theme.palette[name];
return `linear-gradient(135deg, ${c.main}, ${c.dark})`;
};
const disabledStyle = {
'&.Mui-disabled': {
backgroundColor: theme.palette.grey[400]
backgroundColor: theme.palette.grey[400],
backgroundImage: 'none',
borderColor: theme.palette.grey[400],
}
};
// Build per-color contained + outlined overrides
const colors = {
Primary: { gradient: primaryGradient, main: primaryMain, shift: secondaryMain },
Secondary: { gradient: colorGradient('secondary'), main: secondaryMain },
Error: { gradient: colorGradient('error'), main: theme.palette.error.main },
Success: { gradient: colorGradient('success'), main: theme.palette.success.main },
Warning: { gradient: colorGradient('warning'), main: theme.palette.warning.main },
Info: { gradient: colorGradient('info'), main: theme.palette.info.main },
};
const containedOverrides = {};
const outlinedOverrides = {};
Object.entries(colors).forEach(([name, { gradient }]) => {
containedOverrides[`contained${name}`] = {
backgroundImage: gradient,
};
outlinedOverrides[`outlined${name}`] = {
borderColor: 'transparent',
backgroundImage: `linear-gradient(${paperBg}, ${paperBg}), ${gradient}`,
backgroundOrigin: 'border-box',
backgroundClip: 'padding-box, border-box',
'&:hover': {
borderColor: 'transparent',
backgroundColor: 'transparent',
backgroundImage: gradient,
backgroundOrigin: 'border-box',
backgroundClip: 'border-box',
color: '#fff',
},
};
});
return {
MuiButton: {
defaultProps: {
@@ -24,27 +71,40 @@ export default function Button(theme) {
textTransform: 'none',
transition: 'all 0.15s ease',
},
// --- Contained: gradient backgrounds ---
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
filter: 'brightness(1.1)',
filter: 'brightness(1.15)',
},
...disabledStyle
},
...containedOverrides,
// --- Outlined: gradient borders ---
outlined: {
borderWidth: '1.5px',
'&:hover': {
borderWidth: '1.5px',
backgroundColor: alpha(primaryMain, 0.08),
},
...disabledStyle
},
...outlinedOverrides,
// --- Text: gradient tint on hover ---
text: {
'&:hover': {
backgroundColor: alpha(primaryMain, 0.06),
},
},
textPrimary: {
'&:hover': {
backgroundImage: `linear-gradient(135deg, ${alpha(primaryMain, 0.06)}, ${alpha(secondaryMain, 0.06)})`,
backgroundColor: 'transparent',
},
},
}
}
};
+31 -2
View File
@@ -1,6 +1,14 @@
// ==============================|| OVERRIDES - CHIP ||============================== //
export default function Chip(theme) {
const primaryMain = theme.palette.primary.main;
const secondaryMain = theme.palette.secondary.main;
const colorGradient = (name) => {
const c = theme.palette[name];
return `linear-gradient(135deg, ${c.main}, ${c.dark})`;
};
return {
MuiChip: {
styleOverrides: {
@@ -10,13 +18,34 @@ export default function Chip(theme) {
boxShadow: 'none'
}
},
colorWarning: {
color: '#000',
outlinedWarning: {
color: theme.palette.warning.dark,
},
sizeLarge: {
fontSize: '1rem',
height: 40
},
// --- Filled chips: gradient backgrounds ---
filledPrimary: {
backgroundImage: `linear-gradient(135deg, ${primaryMain}, ${secondaryMain})`,
},
filledSecondary: {
backgroundImage: colorGradient('secondary'),
},
filledError: {
backgroundImage: colorGradient('error'),
},
filledSuccess: {
backgroundImage: colorGradient('success'),
},
filledWarning: {
backgroundImage: colorGradient('warning'),
color: '#000',
},
filledInfo: {
backgroundImage: colorGradient('info'),
},
// --- Light variant (custom) ---
light: {
color: theme.palette.primary.main,
backgroundColor: theme.palette.primary.lighter,
+2 -2
View File
@@ -28,10 +28,10 @@ const Theme = (colors, darkMode) => {
return {
primary: {
main: purple[400],
main: "#ff64c8",
},
secondary: {
main: indigo[600],
main: "#c864ff",
},
error: {
lighter: red[0],
@@ -624,6 +624,7 @@
"mgmt.urls.edit.advancedSettings.hideFromDashboardCheckbox.hideFromDashboardLabel": "Hide from Dashboard",
"mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderLabel": "Overwrite Host Header (use this to chain resolve request from another server/ip)",
"mgmt.urls.edit.advancedSettings.overwriteHostHeaderInput.overwriteHostHeaderPlaceholder": "Overwrite Host Header",
"mgmt.urls.edit.advancedSettings.skipURLCleanCheckbox.skipURLCleanLabel": "Skip URL Cleaning (preserve double slashes in path)",
"mgmt.urls.edit.advancedSettings.whitelistInboundIpInput.whitelistInboundIpLabel": "Whitelist Inbound IPs and/or IP ranges (comma separated)",
"mgmt.urls.edit.advancedSettings.whitelistInboundIpInput.whitelistInboundIpPlaceholder": "Whitelist Inbound IPs and/or IP ranges (comma separated)",
"mgmt.urls.edit.advancedSettingsTitle": "Advanced Settings",
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+12 -3
View File
@@ -165,6 +165,8 @@ func StartNATS() {
utils.Debug("[NATS] Adding NATS user for device: " + devices.DeviceName + " With API Key: " + devices.APIKey)
username := sanitizeNATSUsername(devices.DeviceName)
// TODO: Agent / users with less permissions
users = append(users, &server.User{
Username: username,
Password: devices.APIKey,
@@ -268,8 +270,9 @@ func StartNATS() {
retries++
continue
}
if NebulaFailedStarting {
utils.Error("[NATS] Nebula failed to start, aborting NATS server setup", nil)
utils.Error("[NATS] Nebula failed to start, aborting NATS server setup retry", nil)
return
}
@@ -320,7 +323,7 @@ func InitNATSClient() {
var err error
retries := 0
if NebulaFailedStarting || !NebulaStarted {
if NebulaFailedStarting {
utils.Error("[NATS] Nebula failed to start, aborting NATS client connection", nil)
return
}
@@ -365,12 +368,18 @@ func InitNATSClient() {
}
if NebulaFailedStarting {
utils.Error("[NATS] Nebula failed to start, aborting NATS client connection", nil)
utils.Error("[NATS] Nebula failed to start, aborting NATS client connection retry", nil)
return
}
time.Sleep(time.Duration(2 * (retries + 1)) * time.Second)
if !NebulaStarted {
retries++
utils.Warn("[NATS] Nebula not started yet, delaying NATS client connection retry")
continue
}
nc, err = natsClient.Connect("nats://localhost:4222",
nats.Secure(&tls.Config{
InsecureSkipVerify: true,
+66
View File
@@ -5,6 +5,8 @@ import (
"encoding/json"
"strings"
"github.com/nats-io/nats.go"
"github.com/azukaar/cosmos-server/src/utils"
)
@@ -94,3 +96,67 @@ func DevicePublicList(w http.ResponseWriter, req *http.Request) {
"data": publicDevices,
})
}
func PublicDeviceListNATS(m *nats.Msg) {
// Connect to the collection
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
defer closeDb()
if errCo != nil {
utils.Error("PublicDeviceListNATS: Database Connect", errCo)
m.Respond([]byte(`{"status":"error","message":"Database error"}`))
return
}
utils.Debug("PublicDeviceListNATS: Fetching devices")
// Find all non-blocked, non-invisible devices
cursor, err := c.Find(nil, map[string]interface{}{
"Blocked": false,
"Invisible": false,
})
defer cursor.Close(nil)
if err != nil {
utils.Error("PublicDeviceListNATS: Error fetching devices", err)
m.Respond([]byte(`{"status":"error","message":"Error fetching devices"}`))
return
}
var devices []utils.ConstellationDevice
if err = cursor.All(nil, &devices); err != nil {
utils.Error("PublicDeviceListNATS: Error decoding devices", err)
m.Respond([]byte(`{"status":"error","message":"Error decoding devices"}`))
return
}
// Convert to public device info with limited fields
publicDevices := make([]PublicDeviceInfo, len(devices))
for i, device := range devices {
publicDevices[i] = PublicDeviceInfo{
DeviceID: device.DeviceName,
DeviceName: device.DeviceName,
User: device.Nickname,
IP: cleanIp(device.IP),
IsLighthouse: device.IsLighthouse,
CosmosNode: device.CosmosNode,
IsRelay: device.IsRelay,
IsExitNode: device.IsExitNode,
PublicHostname: device.PublicHostname,
Port: device.Port,
}
}
// Respond with the list of public device info
response, err := json.Marshal(map[string]interface{}{
"status": "OK",
"data": publicDevices,
})
if err != nil {
utils.Error("PublicDeviceListNATS: Error marshalling response", err)
m.Respond([]byte(`{"status":"error","message":"Error encoding response"}`))
return
}
m.Respond(response)
}
-1
View File
@@ -544,7 +544,6 @@ func ExportLighthouseFromDB() error {
var relays []string
var Blocklist []string
for _, lh := range ll {
utils.Debug("[CACA PROUT] Lighthouse from DB: " + lh.IP + " - " + lh.PublicHostname)
if lh.Blocked {
Blocklist = append(Blocklist, lh.Fingerprint)
} else {
+2
View File
@@ -289,6 +289,8 @@ func SyncNATSClientRouter(nc *nats.Conn) {
m.Respond([]byte(response))
}
})
nc.Subscribe("cosmos._global_.constellation.public-devices", PublicDeviceListNATS)
nc.Subscribe("cosmos._global_.constellation.data.sync-receive", func(m *nats.Msg) {
utils.Log("[NATS] Constellation data sync received")
+23 -4
View File
@@ -4,10 +4,11 @@ import (
"strings"
"strconv"
"time"
"sort"
"github.com/nats-io/nats.go"
"encoding/json"
"sync"
"github.com/azukaar/cosmos-server/src/utils"
)
@@ -263,6 +264,10 @@ func UpdateLocalTunnelCache() {
}
for _, tunnelRoute := range heartbeat.Tunnels {
// Skip tunnels from ourselves
if heartbeat.DeviceName == currentDeviceName {
continue
}
if tunnelRoute.Tunnel == "_ANY_" || tunnelRoute.Tunnel == currentDeviceName {
if existing, ok := byName[tunnelRoute.Name]; ok {
existing.From = append(existing.From, heartbeat.DeviceName)
@@ -283,9 +288,23 @@ func UpdateLocalTunnelCache() {
tunnels = append(tunnels, *t)
}
// Compare old and new cache using JSON stringify
oldJSON, _ := json.Marshal(localTunnelCache)
newJSON, _ := json.Marshal(tunnels)
// Compare old and new cache using sorted copies for consistent comparison
sortTunnelsForComparison := func(t []utils.ConstellationTunnel) []utils.ConstellationTunnel {
copied := make([]utils.ConstellationTunnel, len(t))
for i, tunnel := range t {
copied[i] = tunnel
copied[i].From = make([]string, len(tunnel.From))
copy(copied[i].From, tunnel.From)
sort.Strings(copied[i].From)
}
sort.Slice(copied, func(i, j int) bool {
return copied[i].Route.Name < copied[j].Route.Name
})
return copied
}
oldJSON, _ := json.Marshal(sortTunnelsForComparison(localTunnelCache))
newJSON, _ := json.Marshal(sortTunnelsForComparison(tunnels))
localTunnelCache = tunnels
lastCacheUpdate = time.Now()
+53 -31
View File
@@ -232,30 +232,44 @@ func tokenMiddleware(next http.Handler) http.Handler {
})
}
func SecureAPI(userRouter *mux.Router, public bool, publicCors bool) {
func SecureAPI(userRouter *mux.Router, public bool, publicCors bool, strict bool) {
if(!public) {
userRouter.Use(tokenMiddleware)
}
userRouter.Use(proxy.SmartShieldMiddleware(
"__COSMOS",
utils.ProxyRouteConfig{
Name: "Cosmos-Internal",
SmartShield: utils.SmartShieldPolicy{
Enabled: true,
PolicyStrictness: 1,
PerUserRequestLimit: 12000,
if(strict) {
userRouter.Use(proxy.SmartShieldMiddleware(
"__COSMOS",
utils.ProxyRouteConfig{
Name: "Cosmos-Internal-login",
SmartShield: utils.SmartShieldPolicy{
Enabled: true,
PolicyStrictness: 1,
PerUserRequestLimit: 10000,
},
},
},
))
))
} else {
userRouter.Use(proxy.SmartShieldMiddleware(
"__COSMOS",
utils.ProxyRouteConfig{
Name: "Cosmos-Internal",
SmartShield: utils.SmartShieldPolicy{
Enabled: true,
PolicyStrictness: 2,
},
},
))
}
if(publicCors || public) {
userRouter.Use(utils.PublicCORS)
}
userRouter.Use(utils.MiddlewareTimeout(45 * time.Second))
userRouter.Use(httprate.Limit(180, 1*time.Minute,
userRouter.Use(httprate.Limit(500, 1*time.Minute,
httprate.WithKeyFuncs(httprate.KeyByIP),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
utils.Error("Too many requests. Throttling", nil)
utils.HTTPError(w, "Too many requests",
http.StatusTooManyRequests, "HTTP003")
@@ -430,7 +444,7 @@ func InitServer() *mux.Router {
utils.Log("Initialising HTTP(S) Router and all routes")
router := mux.NewRouter().StrictSlash(true)
router := mux.NewRouter().StrictSlash(true).SkipClean(true)
router.Use(utils.BlockBannedIPs)
@@ -449,22 +463,26 @@ func InitServer() *mux.Router {
}
logoAPI := router.PathPrefix("/logo").Subrouter()
SecureAPI(logoAPI, true, true)
SecureAPI(logoAPI, true, true, false)
logoAPI.HandleFunc("/", SendLogo)
srapi := router.PathPrefix("/cosmos").Subrouter()
srapi.Use(utils.ContentTypeMiddleware("application/json"))
srapiStrict := router.PathPrefix("/cosmos").Subrouter()
srapiStrict.Use(utils.ContentTypeMiddleware("application/json"))
srapi.HandleFunc("/api/login", user.UserLogin)
srapi.HandleFunc("/api/sudo", user.UserSudo)
srapi.HandleFunc("/api/password-reset", user.ResetPassword)
srapi.HandleFunc("/api/mfa", user.API2FA)
srapiStrict.HandleFunc("/api/login", user.UserLogin)
srapiStrict.HandleFunc("/api/sudo", user.UserSudo)
srapiStrict.HandleFunc("/api/password-reset", user.ResetPassword)
srapiStrict.HandleFunc("/api/mfa", user.API2FA)
srapiStrict.HandleFunc("/api/register", user.UserRegister)
srapiStrict.HandleFunc("/api/newInstall", NewInstallRoute)
srapi.HandleFunc("/api/status", StatusRoute)
srapi.HandleFunc("/api/can-send-email", CanSendEmail)
srapi.HandleFunc("/api/newInstall", NewInstallRoute)
srapi.HandleFunc("/api/logout", user.UserLogout)
srapi.HandleFunc("/api/register", user.UserRegister)
srapi.HandleFunc("/api/dns", GetDNSRoute)
srapi.HandleFunc("/api/dns-check", CheckDNSRoute)
srapi.HandleFunc("/api/favicon", GetFavicon)
@@ -616,18 +634,22 @@ func InitServer() *mux.Router {
srapiAdmin.Use(utils.Restrictions(config.AdminConstellationOnly, config.AdminWhitelistIPs))
srapi.Use(utils.SetSecurityHeaders)
srapiStrict.Use(utils.SetSecurityHeaders)
srapiAdmin.Use(utils.SetSecurityHeaders)
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
srapi.Use(utils.EnsureHostname)
srapiStrict.Use(utils.EnsureHostname)
srapiAdmin.Use(utils.EnsureHostname)
srapi.Use(utils.EnsureHostnameCosmosAPI)
srapiStrict.Use(utils.EnsureHostnameCosmosAPI)
srapiAdmin.Use(utils.EnsureHostnameCosmosAPI)
}
SecureAPI(srapi, false, false)
SecureAPI(srapiAdmin, false, false)
SecureAPI(srapiStrict, false, false, true)
SecureAPI(srapi, false, false, false)
SecureAPI(srapiAdmin, false, false, false)
pwd, err := os.Getwd()
if err != nil {
@@ -642,7 +664,7 @@ func InitServer() *mux.Router {
// fs := http.FileServer(http.Dir(pwd + "/static"))
uirouter := router.PathPrefix("/cosmos-ui").Subrouter()
uirouter.Use(utils.SetSecurityHeaders)
SecureAPI(uirouter, true, true)
SecureAPI(uirouter, true, true, false)
uirouter.PathPrefix("/").Handler(http.StripPrefix("/cosmos-ui", utils.SPAHandler(pwd + "/static")))
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
@@ -650,28 +672,28 @@ func InitServer() *mux.Router {
}
OpenIDDetect := router.PathPrefix("/").Subrouter()
SecureAPI(OpenIDDetect, true, true)
authorizationserver.RegisterHandlersDetect(OpenIDDetect, srapi)
SecureAPI(OpenIDDetect, true, true, false)
authorizationserver.RegisterHandlersDetect(OpenIDDetect, srapiStrict)
router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig)
wellKnownRouter := router.PathPrefix("/").Subrouter()
SecureAPI(wellKnownRouter, true, true)
SecureAPI(wellKnownRouter, true, true, false)
userRouter := router.PathPrefix("/oauth2").Subrouter()
SecureAPI(userRouter, false, true)
SecureAPI(userRouter, false, true, true)
serverRouter := router.PathPrefix("/oauth2").Subrouter()
SecureAPI(serverRouter, true, true)
SecureAPI(serverRouter, true, true, true)
authorizationserver.RegisterHandlers(wellKnownRouter, userRouter, serverRouter)
router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/cosmos-ui/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/cosmos-ui/", http.StatusTemporaryRedirect)
}))
router.HandleFunc("/cosmos-ui", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/cosmos-ui/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/cosmos-ui/", http.StatusTemporaryRedirect)
}))
return router
+1 -1
View File
@@ -331,7 +331,7 @@ func PingURL(w http.ResponseWriter, req *http.Request) {
func SendLogo(w http.ResponseWriter, req *http.Request) {
pwd,_ := os.Getwd()
imgsrc := "Logo.png"
imgsrc := "Logo2.png"
Logo, err := ioutil.ReadFile(pwd + "/" + imgsrc)
if err != nil {
utils.Error("Logo", err)
+28 -15
View File
@@ -107,6 +107,17 @@ func startProxy(listenAddr string, target string, proxyInfo *ProxyInfo, isHTTPPr
var listener net.Listener
var packetConn net.PacketConn
// Ensure we always signal completion, even on early errors
defer func() {
if listener != nil {
listener.Close()
}
if packetConn != nil {
packetConn.Close()
}
close(proxyInfo.stopped)
}()
switch listenProtocol {
case "tcp":
listener, err = net.Listen("tcp", listenAddr)
@@ -122,16 +133,6 @@ func startProxy(listenAddr string, target string, proxyInfo *ProxyInfo, isHTTPPr
return
}
defer func() {
if listener != nil {
listener.Close()
}
if packetConn != nil {
packetConn.Close()
}
close(proxyInfo.stopped)
}()
utils.Log("[SocketProxy] Proxy listening on "+listenAddr+", forwarding to "+listenProtocol+"://"+target)
if listenProtocol == "tcp" {
@@ -171,9 +172,9 @@ func handleTCPProxy(listener net.Listener, target string, proxyInfo *ProxyInfo,
} else {
shieldedClient = client
}
utils.Debug("[SocketProxy] New TCP connection accepted on " + listenAddr)
server, err := net.Dial("tcp", target)
if err != nil {
utils.Error("[SocketProxy] Failed to connect to TCP server", err)
@@ -393,12 +394,24 @@ func InitInternalSocketProxy() {
portpair.To = "localhost:" + HTTPPort
}
expectedPorts = append(expectedPorts, portpair)
// Check if this port is already in the list (avoid duplicates)
alreadyExists := false
for _, existing := range expectedPorts {
if existing.From == portpair.From {
alreadyExists = true
break
}
}
if alreadyExists {
utils.MajorError("[SocketProxy] Duplicate port detected: "+portpair.From+". Multiple routes are trying to use the same port.", nil)
} else {
expectedPorts = append(expectedPorts, portpair)
}
}
}
}
StopAllProxies()
go initInternalPortProxy(expectedPorts)
initInternalPortProxy(expectedPorts)
}
+4 -4
View File
@@ -410,19 +410,19 @@ func TCPSmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) fun
policy.PerUserTimeBudget = 2 * 60 * 60 * 1000 // 2 hours
}
if(policy.PerUserRequestLimit == 0) {
policy.PerUserRequestLimit = 12000 // 150 requests per minute
policy.PerUserRequestLimit = 18000
}
if(policy.PerUserByteLimit == 0) {
policy.PerUserByteLimit = 150 * 1024 * 1024 * 1024 // 150GB
policy.PerUserByteLimit = 200 * 1024 * 1024 * 1024 // 200GB
}
if(policy.PolicyStrictness == 0) {
policy.PolicyStrictness = 2 // NORMAL
}
if(policy.PerUserSimultaneous == 0) {
policy.PerUserSimultaneous = 24
policy.PerUserSimultaneous = 100
}
if(policy.MaxGlobalSimultaneous == 0) {
policy.MaxGlobalSimultaneous = 250
policy.MaxGlobalSimultaneous = 2000
}
if(policy.PrivilegedGroups == 0) {
policy.PrivilegedGroups = utils.ADMIN
+5 -2
View File
@@ -123,21 +123,24 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, DisableHeaderHa
targetIP, err := docker.GetContainerIPByName(targetHost)
if err != nil {
utils.Error("Create Route", err)
utils.Error("Director Route", err)
}
utils.Debug("Dockerless Target IP: " + targetIP)
req.URL.Host = targetIP + ":" + targetURL.Port()
}
utils.Debug("Request to backend: " + req.URL.String())
req.URL.Path, req.URL.RawPath = joinURLPath(targetURL, req.URL)
if urlQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = urlQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = urlQuery + "&" + req.URL.RawQuery
}
utils.Debug("Request to backend: " + req.URL.String())
req.Header.Set("X-Forwarded-Proto", originalScheme)
if(originalScheme == "https") {
+40 -2
View File
@@ -2,10 +2,12 @@ package proxy
import (
"net/http"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
"net/url"
"github.com/azukaar/cosmos-server/src/user"
"github.com/azukaar/cosmos-server/src/constellation"
@@ -14,6 +16,32 @@ import (
"github.com/gorilla/mux"
)
// Borrowed from the net/http package. (Thanks mux!)
func cleanPathMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
if p == "" {
p = "/"
}
if p[0] != '/' {
p = "/" + p
}
np := path.Clean(p)
if p[len(p)-1] == '/' && np != "/" {
np += "/"
}
if np != r.URL.Path {
url := *r.URL
url.Path = np
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
return
}
next.ServeHTTP(w, r)
})
}
func tokenMiddleware(route utils.ProxyRouteConfig) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -95,7 +123,13 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
origin := router.NewRoute()
if route.UseHost {
origin = origin.Host(route.Host)
// If hostname is 0.0.0.0, treat it as a wildcard (match any host with that port)
if strings.Contains(route.Host, ":") && (strings.Split(route.Host, ":")[0] == "0.0.0.0" || route.Host[0] == ":") {
port := strings.Split(route.Host, ":")[1]
origin = origin.Host("{host:[^:]+}:" + port)
} else {
origin = origin.Host(route.Host)
}
if route.Mode == "SERVAPP" || route.Mode == "PROXY" || route.Mode == "REDIRECT" {
urlRoute, err := url.Parse(route.Target)
@@ -199,6 +233,10 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
destination = tokenMiddleware(route)(utils.SetCosmosHeader(destination))
if !route.SkipURLClean {
destination = cleanPathMiddleware(destination)
}
origin.Handler(destination)
utils.Log("Added route: [" + (string)(route.Mode) + "] " + route.Host + route.PathPrefix + " to " + route.Target + "")
+4 -4
View File
@@ -325,19 +325,19 @@ func SmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) func(h
policy.PerUserTimeBudget = 2 * 60 * 60 * 1000 // 2 hours
}
if(policy.PerUserRequestLimit == 0) {
policy.PerUserRequestLimit = 12000 // 150 requests per minute
policy.PerUserRequestLimit = 18000 // 225 requests per minute
}
if(policy.PerUserByteLimit == 0) {
policy.PerUserByteLimit = 150 * 1024 * 1024 * 1024 // 150GB
policy.PerUserByteLimit = 200 * 1024 * 1024 * 1024 // 200GB
}
if(policy.PolicyStrictness == 0) {
policy.PolicyStrictness = 2 // NORMAL
}
if(policy.PerUserSimultaneous == 0) {
policy.PerUserSimultaneous = 24
policy.PerUserSimultaneous = 100
}
if(policy.MaxGlobalSimultaneous == 0) {
policy.MaxGlobalSimultaneous = 250
policy.MaxGlobalSimultaneous = 2000
}
if(policy.PrivilegedGroups == 0) {
policy.PrivilegedGroups = utils.ADMIN
+1
View File
@@ -98,6 +98,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
"LicenceNumber": licenceNumber,
"LicenceNodeNumber": licenceNodeNumber,
"ConfigFolder": absoluteConfigPath,
"ConstellationName": utils.GetMainConfig().ConstellationConfig.ThisDeviceName,
},
})
} else {
+1
View File
@@ -257,6 +257,7 @@ type ProxyRouteConfig struct {
Tunnel string `yaml:"tunnel,omitempty"`
TunneledHost string `yaml:"tunneled_host,omitempty"`
ExtraHeaders map[string]string `yaml:"extra_headers,omitempty"`
SkipURLClean bool `yaml:"skip_url_clean"`
Const_IsTunneled bool `yaml:"-", json:"-"`
}
+9 -8
View File
@@ -453,14 +453,15 @@ func SoftRestartServer() {
RestartHTTPServer()
WaitForAllJobs()
RestartConstellation() // Constellation
InitRemoteStorage() // rclone
InitBackups() // restic
InitSnapRAIDConfig() // snapraid
RestartCRON()
go func() {
WaitForAllJobs()
RestartConstellation() // Constellation
InitRemoteStorage() // rclone
InitBackups() // restic
InitSnapRAIDConfig() // snapraid
RestartCRON()
}()
}
func LetsEncryptValidOnly(hostnames []string, acceptWildcard bool) []string {
BIN
View File
Binary file not shown.