mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2025-12-30 08:50:24 -06:00
[release] v0.19.0-unstable1
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
echo " ---- Build Cosmos ----"
|
||||
|
||||
# Set target architecture for ARM64
|
||||
# export GOOS=linux
|
||||
# export GOARCH=arm64
|
||||
|
||||
rm -rf build
|
||||
|
||||
cp src/update.go src/launcher/update.go
|
||||
|
||||
11
changelog.md
11
changelog.md
@@ -1,3 +1,14 @@
|
||||
## Version 0.19.0
|
||||
- Constellation allows nodes to see and ping each others
|
||||
- Constellation now has a firewall!
|
||||
- Constellation now has exit nodes
|
||||
- Improve docker image cleanup efficiency
|
||||
- Improve support for container network modes in import/export
|
||||
- Fixed the annoying "user unauthenticated" error when opening the homepage after the admin token expired
|
||||
- Fixed issue with exporting hostname when it would be incompatible to re-importing it
|
||||
- Updating network mode now also updates the network-mode label
|
||||
- Default storage path is now /cosmos-storage instead of /usr
|
||||
|
||||
## Version 0.18.4
|
||||
- Fix issue with DB credentials dissapearing
|
||||
- Remove expired discount
|
||||
|
||||
@@ -128,6 +128,15 @@ function ping() {
|
||||
});
|
||||
}
|
||||
|
||||
function pingDevice() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
"status": "ok",
|
||||
"data": 1
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
addDevice,
|
||||
@@ -138,4 +147,5 @@ export {
|
||||
connect,
|
||||
block,
|
||||
ping,
|
||||
pingDevice
|
||||
};
|
||||
@@ -18,6 +18,15 @@ function ping() {
|
||||
}))
|
||||
}
|
||||
|
||||
function pingDevice(deviceId) {
|
||||
return wrap(fetch(`/cosmos/api/constellation/devices/${deviceId}/ping`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function addDevice(device) {
|
||||
return wrap(fetch('/cosmos/api/constellation/devices', {
|
||||
method: 'POST',
|
||||
@@ -128,4 +137,5 @@ export {
|
||||
connect,
|
||||
block,
|
||||
ping,
|
||||
pingDevice,
|
||||
};
|
||||
@@ -139,7 +139,7 @@ const ConfigManagement = () => {
|
||||
|
||||
SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork,
|
||||
SkipPruneImages: config.DockerConfig.SkipPruneImages,
|
||||
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr",
|
||||
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/cosmos-storage",
|
||||
|
||||
Background: config && config.HomepageConfig && config.HomepageConfig.Background,
|
||||
Expanded: config && config.HomepageConfig && config.HomepageConfig.Expanded,
|
||||
@@ -817,7 +817,7 @@ const ConfigManagement = () => {
|
||||
label={t('mgmt.config.docker.defaultDatapathInput.defaultDatapathLabel')}
|
||||
name="DefaultDataPath"
|
||||
formik={formik}
|
||||
placeholder={'/usr'}
|
||||
placeholder={'/cosmos-storage'}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -34,7 +34,7 @@ import defaultport from '../../servapps/defaultport.json';
|
||||
|
||||
import * as API from '../../../api';
|
||||
|
||||
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
|
||||
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label, name = "Target"}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [containers, setContainers] = React.useState([]);
|
||||
@@ -179,7 +179,7 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
|
||||
|
||||
return ( <Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor={name + "-autocomplete"}>{t('mgmt.config.containerPicker.containerNameSelection.containerNameLabel')}</InputLabel>
|
||||
<InputLabel htmlFor={name + "-autocomplete"}>{label || t('mgmt.config.containerPicker.containerNameSelection.containerNameLabel')}</InputLabel>
|
||||
{!loading && <Autocomplete
|
||||
id={name + "-autocomplete"}
|
||||
open={open}
|
||||
|
||||
@@ -75,6 +75,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
|
||||
PublicHostname: '',
|
||||
IsRelay: true,
|
||||
isLighthouse: false,
|
||||
invisible: false,
|
||||
}}
|
||||
|
||||
validationSchema={yup.object({
|
||||
@@ -185,6 +186,12 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
|
||||
formik={formik}
|
||||
/> */}
|
||||
|
||||
<CosmosCheckbox
|
||||
name="invisible"
|
||||
label="Invisible (Other clients won't be able to discover this device)"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
{formik.values.isLighthouse && <>
|
||||
<CosmosFormDivider title={t('mgmt.constellation.setuplighthouseTitle')} />
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import * as API from "../../api";
|
||||
import AddDeviceModal from "./addDevice";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, SyncOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, CircularProgress, IconButton, Stack, Tooltip } from "@mui/material";
|
||||
import { CloudOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, SyncOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, Chip, CircularProgress, IconButton, Stack, Switch, Tooltip } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
import { Formik } from "formik";
|
||||
@@ -22,23 +22,25 @@ import { autoBatchEnhancer } from "@reduxjs/toolkit";
|
||||
|
||||
const getDefaultConstellationHostname = (config) => {
|
||||
// if domain is set, use it
|
||||
if(isDomain(config.HTTPConfig.Hostname)) {
|
||||
if (isDomain(config.HTTPConfig.Hostname)) {
|
||||
return "vpn." + config.HTTPConfig.Hostname;
|
||||
} else {
|
||||
return config.HTTPConfig.Hostname;
|
||||
}
|
||||
}
|
||||
|
||||
export const ConstellationVPN = ({freeVersion}) => {
|
||||
export const ConstellationVPN = ({ freeVersion }) => {
|
||||
const { t } = useTranslation();
|
||||
const [config, setConfig] = useState(null);
|
||||
const [users, setUsers] = useState(null);
|
||||
const [devices, setDevices] = useState(null);
|
||||
const [resynDevice, setResyncDevice] = useState(null); // [nickname, deviceName]
|
||||
const {role} = useClientInfos();
|
||||
const { role } = useClientInfos();
|
||||
const isAdmin = role === "2";
|
||||
const [ping, setPing] = useState(0);
|
||||
const [coStatus, setCoStatus] = React.useState(null);
|
||||
const [devicePingStatus, setDevicePingStatus] = useState({}); // {deviceName: 'loading' | 'success' | 'error'}
|
||||
const [firewallLoading, setFirewallLoading] = useState(null); // deviceName being toggled
|
||||
|
||||
const refreshStatus = () => {
|
||||
API.getStatus().then((res) => {
|
||||
@@ -46,6 +48,69 @@ export const ConstellationVPN = ({freeVersion}) => {
|
||||
});
|
||||
}
|
||||
|
||||
const pingDevices = async (deviceList) => {
|
||||
if (!deviceList || deviceList.length === 0) return;
|
||||
|
||||
// Initialize all devices as loading
|
||||
const initialStatus = {};
|
||||
deviceList.forEach(device => {
|
||||
initialStatus[device.deviceName] = 'loading';
|
||||
});
|
||||
setDevicePingStatus(initialStatus);
|
||||
|
||||
// Ping devices 5 at a time
|
||||
const batchSize = 5;
|
||||
for (let i = 0; i < deviceList.length; i += batchSize) {
|
||||
const batch = deviceList.slice(i, i + batchSize);
|
||||
const pingPromises = batch.map(device =>
|
||||
API.constellation.pingDevice(device.deviceName)
|
||||
.then(res => {
|
||||
setDevicePingStatus(prev => ({
|
||||
...prev,
|
||||
[device.deviceName]: res.data.reachable ? 'success' : 'error'
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
setDevicePingStatus(prev => ({
|
||||
...prev,
|
||||
[device.deviceName]: 'error'
|
||||
}));
|
||||
})
|
||||
);
|
||||
await Promise.all(pingPromises);
|
||||
}
|
||||
};
|
||||
|
||||
const isFirewallBlocked = (deviceName) => {
|
||||
if (!config?.ConstellationConfig?.FirewallBlockedClients) {
|
||||
return false;
|
||||
}
|
||||
return config.ConstellationConfig.FirewallBlockedClients.includes(deviceName);
|
||||
};
|
||||
|
||||
const toggleFirewallBlock = async (deviceName, isBlocked) => {
|
||||
setFirewallLoading(deviceName);
|
||||
|
||||
let newConfig = { ...config };
|
||||
if (!newConfig.ConstellationConfig.FirewallBlockedClients) {
|
||||
newConfig.ConstellationConfig.FirewallBlockedClients = [];
|
||||
}
|
||||
|
||||
if (isBlocked) {
|
||||
// Remove from blocked list
|
||||
newConfig.ConstellationConfig.FirewallBlockedClients =
|
||||
newConfig.ConstellationConfig.FirewallBlockedClients.filter(d => d !== deviceName);
|
||||
} else {
|
||||
// Add to blocked list
|
||||
if (!newConfig.ConstellationConfig.FirewallBlockedClients.includes(deviceName)) {
|
||||
newConfig.ConstellationConfig.FirewallBlockedClients.push(deviceName);
|
||||
}
|
||||
}
|
||||
|
||||
await API.config.set(newConfig);
|
||||
setFirewallLoading(null);
|
||||
};
|
||||
|
||||
let constellationEnabled = config && config.ConstellationConfig.Enabled;
|
||||
|
||||
const refreshConfig = async () => {
|
||||
@@ -53,14 +118,17 @@ export const ConstellationVPN = ({freeVersion}) => {
|
||||
refreshStatus();
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setDevices((await API.constellation.list()).data || []);
|
||||
if(isAdmin)
|
||||
const deviceList = (await API.constellation.list()).data || [];
|
||||
setDevices(deviceList);
|
||||
if (isAdmin)
|
||||
setUsers((await API.users.list()).data || []);
|
||||
else
|
||||
else
|
||||
setUsers([]);
|
||||
|
||||
if(configAsync.data.ConstellationConfig.Enabled) {
|
||||
if (configAsync.data.ConstellationConfig.Enabled) {
|
||||
setPing((await API.constellation.ping()).data ? 2 : 1);
|
||||
// Ping devices after loading
|
||||
pingDevices(deviceList.filter((d) => !d.blocked));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,177 +160,255 @@ export const ConstellationVPN = ({freeVersion}) => {
|
||||
() => setResyncDevice(null)
|
||||
} />
|
||||
}
|
||||
<Stack spacing={2} style={{maxWidth: "1000px", margin: freeVersion ? "auto" : 0}}>
|
||||
<div>
|
||||
{constellationEnabled && coStatus.ConstellationSlaveIPWarning && <Alert severity="error">
|
||||
{coStatus.ConstellationSlaveIPWarning}
|
||||
</Alert>}
|
||||
<Stack spacing={2} style={{ maxWidth: "1000px", margin: freeVersion ? "auto" : 0 }}>
|
||||
<div>
|
||||
{constellationEnabled && coStatus && coStatus.ConstellationSlaveIPWarning && <Alert severity="error">
|
||||
{coStatus.ConstellationSlaveIPWarning}
|
||||
</Alert>}
|
||||
|
||||
{!freeVersion && <Alert severity="info">
|
||||
<Trans i18nKey="mgmt.constellation.setupText"
|
||||
components={[<a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank"></a>, <a href="https://cosmos-cloud.io/clients" target="_blank"></a>]}
|
||||
/>
|
||||
</Alert>}
|
||||
<MainCard title={t('mgmt.constellation.setupTitle')} content={config.constellationIP}>
|
||||
<Stack spacing={2}>
|
||||
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
|
||||
<Alert severity="info">
|
||||
{t('mgmt.constellation.externalTextSlaveNoAdmin')}
|
||||
</Alert>
|
||||
</>}
|
||||
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
|
||||
<Alert severity="info">
|
||||
{t('mgmt.constellation.externalText')}
|
||||
</Alert>
|
||||
</>}
|
||||
{!constellationEnabled && !isAdmin && <>
|
||||
<Alert severity="info">
|
||||
{t('mgmt.constellation.setupTextNoAdmin')}
|
||||
</Alert>
|
||||
</>}
|
||||
{(isAdmin || constellationEnabled) && <Formik
|
||||
enableReinitialize
|
||||
initialValues={{
|
||||
Enabled: config.ConstellationConfig.Enabled,
|
||||
PrivateNode: config.ConstellationConfig.PrivateNode,
|
||||
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
|
||||
SyncNodes: !config.ConstellationConfig.DoNotSyncNodes,
|
||||
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
|
||||
getDefaultConstellationHostname(config)
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
let newConfig = { ...config };
|
||||
newConfig.ConstellationConfig.Enabled = values.Enabled;
|
||||
newConfig.ConstellationConfig.PrivateNode = values.PrivateNode;
|
||||
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
|
||||
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
|
||||
newConfig.ConstellationConfig.DoNotSyncNodes = !values.SyncNodes;
|
||||
setTimeout(() => {
|
||||
refreshConfig();
|
||||
}, 1500);
|
||||
return API.config.set(newConfig);
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
{isAdmin && constellationEnabled && <Stack spacing={2} direction="row">
|
||||
<Button
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
await API.constellation.restart();
|
||||
}}
|
||||
>
|
||||
{t('mgmt.constellation.restartButton')}
|
||||
</Button>
|
||||
<ApiModal callback={API.constellation.getLogs} label={t('mgmt.constellation.showLogsButton')} />
|
||||
<ApiModal callback={API.constellation.getConfig} label={t('mgmt.constellation.showConfigButton')} />
|
||||
<ConfirmModal
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
label={t('mgmt.constellation.resetLabel')}
|
||||
content={t('mgmt.constellation.resetText')}
|
||||
callback={async () => {
|
||||
await API.constellation.reset();
|
||||
refreshConfig();
|
||||
}}
|
||||
/>
|
||||
</Stack>}
|
||||
|
||||
{constellationEnabled && <div>
|
||||
{t('mgmt.constellation.constStatus')}: {[
|
||||
<CircularProgress color="inherit" size={20} />,
|
||||
<span style={{color: "red"}}>{t('mgmt.constellation.constStatusDown')}</span>,
|
||||
<span style={{color: "green"}}>{t('mgmt.constellation.constStatusConnected')}</span>,
|
||||
][ping]}
|
||||
{!freeVersion && <Alert severity="info">
|
||||
<Trans i18nKey="mgmt.constellation.setupText"
|
||||
components={[<a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank"></a>, <a href="https://cosmos-cloud.io/clients" target="_blank"></a>]}
|
||||
/>
|
||||
</Alert>}
|
||||
<MainCard title={t('mgmt.constellation.setupTitle')} content={config.constellationIP}>
|
||||
<Stack spacing={2}>
|
||||
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
|
||||
<Alert severity="info">
|
||||
{t('mgmt.constellation.externalTextSlaveNoAdmin')}
|
||||
</Alert>
|
||||
</>}
|
||||
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
|
||||
<Alert severity="info">
|
||||
{t('mgmt.constellation.externalText')}
|
||||
</Alert>
|
||||
</>}
|
||||
{!constellationEnabled && !isAdmin && <>
|
||||
<Alert severity="info">
|
||||
{t('mgmt.constellation.setupTextNoAdmin')}
|
||||
</Alert>
|
||||
</>}
|
||||
{(isAdmin || constellationEnabled) && <Formik
|
||||
enableReinitialize
|
||||
initialValues={{
|
||||
Enabled: config.ConstellationConfig.Enabled,
|
||||
PrivateNode: config.ConstellationConfig.PrivateNode,
|
||||
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
|
||||
SyncNodes: !config.ConstellationConfig.DoNotSyncNodes,
|
||||
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
|
||||
getDefaultConstellationHostname(config)
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
let newConfig = { ...config };
|
||||
newConfig.ConstellationConfig.Enabled = values.Enabled;
|
||||
newConfig.ConstellationConfig.PrivateNode = values.PrivateNode;
|
||||
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
|
||||
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
|
||||
newConfig.ConstellationConfig.DoNotSyncNodes = !values.SyncNodes;
|
||||
setTimeout(() => {
|
||||
refreshConfig();
|
||||
}, 1500);
|
||||
return API.config.set(newConfig);
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
{isAdmin && constellationEnabled && <Stack spacing={2} direction="row">
|
||||
<Button
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
await API.constellation.restart();
|
||||
}}
|
||||
>
|
||||
{t('mgmt.constellation.restartButton')}
|
||||
</Button>
|
||||
<ApiModal callback={API.constellation.getLogs} label={t('mgmt.constellation.showLogsButton')} />
|
||||
<ApiModal callback={API.constellation.getConfig} label={t('mgmt.constellation.showConfigButton')} />
|
||||
<ConfirmModal
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
label={t('mgmt.constellation.resetLabel')}
|
||||
content={t('mgmt.constellation.resetText')}
|
||||
callback={async () => {
|
||||
await API.constellation.reset();
|
||||
refreshConfig();
|
||||
}}
|
||||
/>
|
||||
</Stack>}
|
||||
|
||||
<IconButton onClick={async () => {
|
||||
setPing(0);
|
||||
setPing((await API.constellation.ping()).data ? 2 : 1);
|
||||
}}>
|
||||
<SyncOutlined />
|
||||
</IconButton>
|
||||
</div>}
|
||||
{constellationEnabled && <div>
|
||||
{t('mgmt.constellation.constStatus')}: {[
|
||||
<CircularProgress color="inherit" size={20} />,
|
||||
<span style={{ color: "red" }}>{t('mgmt.constellation.constStatusDown')}</span>,
|
||||
<span style={{ color: "green" }}>{t('mgmt.constellation.constStatusConnected')}</span>,
|
||||
][ping]}
|
||||
|
||||
{!freeVersion && <>
|
||||
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="Enabled" label={t('mgmt.constellation.setup.enabledCheckbox')} />
|
||||
|
||||
{constellationEnabled && !config.ConstellationConfig.SlaveMode && <>
|
||||
{formik.values.Enabled && <>
|
||||
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="IsRelay" label={t('mgmt.constellation.setup.relayRequests.label')} />
|
||||
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="PrivateNode" label={t('mgmt.constellation.setup.privNode.label')} />
|
||||
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="SyncNodes" label={t('mgmt.constellation.setup.dataSync.label')} />
|
||||
{!formik.values.PrivateNode && <>
|
||||
<Alert severity="info"><Trans i18nKey="mgmt.constellation.setup.hostnameInfo" /></Alert>
|
||||
<CosmosInputText disabled={!isAdmin} formik={formik} name="ConstellationHostname" label={'Constellation '+t('global.hostname')} />
|
||||
<IconButton onClick={async () => {
|
||||
setPing(0);
|
||||
setPing((await API.constellation.ping()).data ? 2 : 1);
|
||||
}}>
|
||||
<SyncOutlined />
|
||||
</IconButton>
|
||||
</div>}
|
||||
|
||||
{!freeVersion && <>
|
||||
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="Enabled" label={t('mgmt.constellation.setup.enabledCheckbox')} />
|
||||
|
||||
{constellationEnabled && !config.ConstellationConfig.SlaveMode && <>
|
||||
{formik.values.Enabled && <>
|
||||
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="SyncNodes" label={t('mgmt.constellation.setup.dataSync.label')} />
|
||||
{devices.length > 0 && <Alert severity="warning">{t('mgmt.constellation.setup.deviceConnectedWarn')}</Alert>}
|
||||
<CosmosCheckbox disabled={!isAdmin || devices.length > 0} formik={formik} name="IsRelay" label={t('mgmt.constellation.setup.relayRequests.label')} />
|
||||
<CosmosCheckbox disabled={!isAdmin || devices.length > 0} formik={formik} name="PrivateNode" label={t('mgmt.constellation.setup.privNode.label')} />
|
||||
{!formik.values.PrivateNode && <>
|
||||
<Alert severity="info"><Trans i18nKey="mgmt.constellation.setup.hostnameInfo" /></Alert>
|
||||
<CosmosInputText disabled={!isAdmin || devices.length > 0} formik={formik} name="ConstellationHostname" label={'Constellation ' + t('global.hostname')} />
|
||||
</>}
|
||||
</>}
|
||||
</>}
|
||||
|
||||
{isAdmin && <><LoadingButton
|
||||
disableElevation
|
||||
loading={formik.isSubmitting}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{t('global.saveAction')}
|
||||
</LoadingButton>
|
||||
</>}
|
||||
</>}
|
||||
</>}
|
||||
</>}
|
||||
{isAdmin && <><UploadButtons
|
||||
accept=".yml,.yaml"
|
||||
label={config.ConstellationConfig.SlaveMode ?
|
||||
t('mgmt.constellation.setup.externalConfig.slaveMode.label')
|
||||
: t('mgmt.constellation.setup.externalConfig.label')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
OnChange={async (e) => {
|
||||
let file = e.target.files[0];
|
||||
await API.constellation.connect(file);
|
||||
setTimeout(() => {
|
||||
refreshConfig();
|
||||
}, 1000);
|
||||
}}
|
||||
/></>}
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</div>
|
||||
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
|
||||
<CosmosFormDivider title={"Devices"} />
|
||||
<PrettyTableView
|
||||
data={devices.filter((d) => !d.blocked)}
|
||||
getKey={(r) => r.deviceName}
|
||||
buttons={[
|
||||
<AddDeviceModal users={users} config={config} refreshConfig={refreshConfig} devices={devices} />,
|
||||
<Button
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
pingDevices(devices.filter((d) => !d.blocked));
|
||||
}}
|
||||
>
|
||||
{t('mgmt.constellation.setup.repingAll')}
|
||||
</Button>
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: '',
|
||||
field: getIcon,
|
||||
},
|
||||
{
|
||||
title: t('mgmt.constellation.setup.deviceName.label'),
|
||||
field: (r) => {
|
||||
|
||||
{isAdmin && <><LoadingButton
|
||||
disableElevation
|
||||
loading={formik.isSubmitting}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{t('global.saveAction')}
|
||||
</LoadingButton>
|
||||
</>}
|
||||
</>}
|
||||
{isAdmin && <><UploadButtons
|
||||
accept=".yml,.yaml"
|
||||
label={config.ConstellationConfig.SlaveMode ?
|
||||
t('mgmt.constellation.setup.externalConfig.slaveMode.label')
|
||||
: t('mgmt.constellation.setup.externalConfig.label')}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
OnChange={async (e) => {
|
||||
let file = e.target.files[0];
|
||||
await API.constellation.connect(file);
|
||||
setTimeout(() => {
|
||||
refreshConfig();
|
||||
}, 1000);
|
||||
}}
|
||||
/></>}
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</div>
|
||||
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
|
||||
<CosmosFormDivider title={"Devices"} />
|
||||
<PrettyTableView
|
||||
data={devices.filter((d) => !d.blocked)}
|
||||
getKey={(r) => r.deviceName}
|
||||
buttons={[
|
||||
<AddDeviceModal users={users} config={config} refreshConfig={refreshConfig} devices={devices}/>,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
title: '',
|
||||
field: getIcon,
|
||||
const status = devicePingStatus[r.deviceName];
|
||||
let res = "";
|
||||
|
||||
if (status === 'loading') {
|
||||
res = <CircularProgress size={16} />;
|
||||
} else if (status === 'success') {
|
||||
res = "🟢";
|
||||
} else if (status === 'error') {
|
||||
res = "🔴";
|
||||
}
|
||||
|
||||
return <strong>{res} {r.deviceName}</strong>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('mgmt.constellation.setup.deviceName.label'),
|
||||
field: (r) => <strong>{r.deviceName}</strong>,
|
||||
title: t('mgmt.constellation.setup.owner.label'),
|
||||
field: (r) => <strong>{r.nickname}</strong>,
|
||||
},
|
||||
{
|
||||
title: t('mgmt.constellation.setup.owner.label'),
|
||||
field: (r) => <strong>{r.nickname}</strong>,
|
||||
title: t('mgmt.storage.typeTitle'),
|
||||
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
|
||||
},
|
||||
{
|
||||
title: t('mgmt.storage.typeTitle'),
|
||||
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
|
||||
title: t('mgmt.constellation.setup.ipTitle'),
|
||||
screenMin: 'md',
|
||||
field: (r) => r.ip,
|
||||
},
|
||||
{
|
||||
title: t('mgmt.constellation.setup.ipTitle'),
|
||||
screenMin: 'md',
|
||||
field: (r) => r.ip,
|
||||
title: t('mgmt.constellation.setup.firewallStatus'),
|
||||
field: (r) => {
|
||||
const blocked = isFirewallBlocked(r.deviceName);
|
||||
const isLoading = firewallLoading === r.deviceName;
|
||||
|
||||
if (isLoading) {
|
||||
return <Chip
|
||||
label={
|
||||
<div>
|
||||
<CircularProgress size={16} style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Updating...
|
||||
</div>
|
||||
}
|
||||
color="default"
|
||||
/>;
|
||||
}
|
||||
|
||||
return blocked ? <Chip
|
||||
label={
|
||||
<div>
|
||||
<Switch size="small" style={{ verticalAlign: "middle", marginRight: 4 }} />
|
||||
Blocked
|
||||
</div>
|
||||
}
|
||||
color="error"
|
||||
onClick={() => toggleFirewallBlock(r.deviceName, blocked)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/> : <Chip
|
||||
label={
|
||||
<div>
|
||||
<Switch
|
||||
size="small"
|
||||
sx={{
|
||||
marginTop: "-3px",
|
||||
'& .MuiSwitch-switchBase.Mui-checked': {
|
||||
color: "white",
|
||||
},
|
||||
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
}}
|
||||
checked
|
||||
/>
|
||||
Allowed
|
||||
</div>
|
||||
}
|
||||
color="success"
|
||||
onClick={() => toggleFirewallBlock(r.deviceName, blocked)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
@@ -281,14 +427,14 @@ export const ConstellationVPN = ({freeVersion}) => {
|
||||
</>
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>}
|
||||
</Stack>
|
||||
]}
|
||||
/>
|
||||
</>}
|
||||
</Stack>
|
||||
</> : <center>
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
</center>}
|
||||
|
||||
{freeVersion && config && !constellationEnabled && <VPNSalesPage />}
|
||||
{freeVersion && config && !constellationEnabled && <VPNSalesPage />}
|
||||
</>
|
||||
};
|
||||
@@ -124,7 +124,7 @@ const convertDockerCompose = (config, serviceName, dockerCompose, setYmlError) =
|
||||
if(doc.services[key].volumes)
|
||||
Object.values(doc.services[key].volumes).forEach((volume) => {
|
||||
if (volume.source && volume.source[0] === '.') {
|
||||
let defaultPath = (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/usr"
|
||||
let defaultPath = (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/cosmos-storage"
|
||||
volume.source = defaultPath + volume.source.replace('.', '');
|
||||
}
|
||||
});
|
||||
@@ -324,7 +324,7 @@ const convertDockerCompose = (config, serviceName, dockerCompose, setYmlError) =
|
||||
|
||||
// for each network mode that are container, add a label and remove hostname
|
||||
Object.keys(doc.services).forEach((key) => {
|
||||
if (doc.services[key].network_mode && doc.services[key].network_mode.startsWith('container:')) {
|
||||
if (doc.services[key].network_mode && (doc.services[key].network_mode.startsWith('service:') || doc.services[key].network_mode.startsWith('container:'))) {
|
||||
doc.services[key].labels = doc.services[key].labels || {};
|
||||
doc.services[key].labels['cosmos-force-network-mode'] = doc.services[key].network_mode;
|
||||
|
||||
@@ -549,7 +549,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
|
||||
Passwords: passwords,
|
||||
CPU_ARCH: API.CPU_ARCH,
|
||||
CPU_AVX: API.CPU_AVX,
|
||||
DefaultDataPath: (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/usr",
|
||||
DefaultDataPath: (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/cosmos-storage",
|
||||
});
|
||||
|
||||
let jsoned;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { NetworksColumns } from '../networks';
|
||||
import NewNetworkButton from '../createNetwork';
|
||||
import LinkContainersButton from '../linkContainersButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CosmosContainerPicker } from '../../config/users/containerPicker';
|
||||
|
||||
const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange, OnConnect, OnDisconnect }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -93,6 +94,11 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
|
||||
initialValues={{
|
||||
networkMode: containerInfo.HostConfig.NetworkMode,
|
||||
ports: getPortBindings(),
|
||||
Container: (() => {
|
||||
if(!containerInfo || !containerInfo.HostConfig || !containerInfo.HostConfig.NetworkMode) return "";
|
||||
if(!containerInfo.HostConfig.NetworkMode.startsWith("container:")) return "";
|
||||
return containerInfo.HostConfig.NetworkMode.replace("container:", "");
|
||||
})()
|
||||
}}
|
||||
validate={(values) => {
|
||||
const errors = {};
|
||||
@@ -142,16 +148,29 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
|
||||
{t('mgmt.servApps.networks.containerotRunningWarning')}
|
||||
</Alert>
|
||||
)}
|
||||
{isForceSecure && (
|
||||
{/* {isForceSecure && (
|
||||
<Alert severity="warning" style={{ marginBottom: '0px' }}>
|
||||
{t('mgmt.servApps.networks.forcedSecurityWarning')}
|
||||
</Alert>
|
||||
)}
|
||||
)} */}
|
||||
<CosmosInputText
|
||||
label={t('mgmt.servApps.networks.modeInput.modeLabel')}
|
||||
name="networkMode"
|
||||
placeholder={'default'}
|
||||
formik={formik}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('Container', '');
|
||||
}}
|
||||
/>
|
||||
<CosmosContainerPicker
|
||||
formik={formik}
|
||||
onTargetChange={(_, name) => {
|
||||
if(!name) return;
|
||||
formik.setFieldValue('networkMode', `container:${name}`);
|
||||
}}
|
||||
name='Container'
|
||||
label={t('mgmt.servApps.networks.useAsVPN')}
|
||||
nameOnly
|
||||
/>
|
||||
<CosmosFormDivider title={t('mgmt.servApps.networks.exposePortsTitle')} />
|
||||
<div>
|
||||
|
||||
@@ -354,11 +354,12 @@ const ServApps = ({stack}) => {
|
||||
Ports
|
||||
</Typography>
|
||||
<Stack style={noOver} margin={1} direction="row" spacing={1}>
|
||||
{app.ports.filter(p => p.IP != '::').map((port) => {
|
||||
{console.log(app.ports)}
|
||||
{app.ports && (app.ports.filter(p => p && p.IP != '::').map((port) => {
|
||||
return <Tooltip title={port.PublicPort ? 'Warning, this port is publicly accessible' : ''}>
|
||||
<Chip style={{ fontSize: '80%' }} label={(port.PublicPort ? (port.PublicPort + ":") : '') + port.PrivatePort} color={port.PublicPort ? 'warning' : 'default'} />
|
||||
</Tooltip>
|
||||
})}
|
||||
}))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
|
||||
@@ -19,15 +19,26 @@ function useClientInfos() {
|
||||
// Try to parse the cookie into a JavaScript object
|
||||
clientInfos = cookies['client-infos'].split(',');
|
||||
|
||||
if(clientInfos.length !== 3) {
|
||||
if(clientInfos.length <= 3) {
|
||||
window.location.href = '/cosmos-ui/logout';
|
||||
}
|
||||
|
||||
return {
|
||||
let res = {
|
||||
nickname: clientInfos[0],
|
||||
userRole: clientInfos[1],
|
||||
role: clientInfos[2]
|
||||
};
|
||||
|
||||
if(clientInfos.length > 3) {
|
||||
let roleUntil = new Date(parseInt(clientInfos[3], 10) * 1000);
|
||||
let currentDate = new Date();
|
||||
|
||||
if(roleUntil < currentDate && res.userRole == "2") {
|
||||
res.userRole = "1";
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Error parsing client-infos cookie:', error);
|
||||
return {
|
||||
|
||||
@@ -273,6 +273,9 @@
|
||||
"mgmt.constellation.setup.relayRequests.label": "Relay requests via this Node",
|
||||
"mgmt.constellation.setup.unsafeRoutesText": "Coming soon. This feature will allow you to tunnel your traffic through your devices to things outside of your constellation.",
|
||||
"mgmt.constellation.setup.unsafeRoutesTitle": "Unsafe Routes",
|
||||
"mgmt.constellation.setup.repingAll": "Ping All",
|
||||
"mgmt.constellation.setup.deviceConnectedWarn": "The following settings cannot be changed once clients have been created. Reset your network to change them.",
|
||||
"mgmt.constellation.setup.firewallStatus": "Firewall",
|
||||
"mgmt.constellation.setupText": "Constellation is a VPN that runs inside your Cosmos network. It automatically connects all your devices together, and allows you to access them from anywhere. Please refer to the <0>documentation</0> for more information. In order to connect, please use the <1>Constellation App</1>",
|
||||
"mgmt.constellation.setupTitle": "Constellation Setup",
|
||||
"mgmt.constellation.setuplighthouseTitle": "Lighthouse Setup",
|
||||
@@ -363,6 +366,7 @@
|
||||
"mgmt.servApps.networks.removedNetConnectedEitherRecreate": "Either re-create it or",
|
||||
"mgmt.servApps.networks.removedNetConnectedError": "You are connected to a network that has been removed:",
|
||||
"mgmt.servApps.networks.updatePortsButton": "Update Ports",
|
||||
"mgmt.servApps.networks.useAsVPN": "Use this container as VPN or Network controller",
|
||||
"mgmt.servApps.newChip.newLabel": "New",
|
||||
"mgmt.servApps.newContainer.chooseUrl": "Choose URL for",
|
||||
"mgmt.servApps.newContainer.cosmosOutdatedError": "This service requires a newer version of Cosmos. Please update Cosmos to install this service.",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.18.0-unstable8",
|
||||
"version": "0.19.0-unstable1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cosmos-server",
|
||||
"version": "0.18.0-unstable8",
|
||||
"version": "0.19.0-unstable1",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.18.4",
|
||||
"version": "0.19.0-unstable1",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
||||
@@ -396,14 +396,17 @@ func SlaveConfigSync(newConfig string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
endpoint := configMap["cstln_config_endpoint"]
|
||||
rawEndpoint := configMap["cstln_config_endpoint"]
|
||||
apiKey := configMap["cstln_api_key"]
|
||||
|
||||
if endpoint == nil || apiKey == nil {
|
||||
if rawEndpoint == nil || apiKey == nil {
|
||||
utils.Error("SlaveConfigSync: Invalid slave config file for resync", nil)
|
||||
return false, errors.New("Invalid slave config file for resync")
|
||||
}
|
||||
|
||||
endpoint := rawEndpoint.(string)
|
||||
endpoint += "cosmos/api/constellation/config-sync"
|
||||
|
||||
// utils.Log("SlaveConfigSync: Fetching config from " + endpoint.(string))
|
||||
|
||||
// fetch the config from the endpoint with Authorization header
|
||||
|
||||
@@ -21,6 +21,7 @@ type DeviceCreateRequestJSON struct {
|
||||
IsRelay bool `json:"isRelay",omitempty`
|
||||
PublicHostname string `json:"PublicHostname",omitempty`
|
||||
Port string `json:"port",omitempty`
|
||||
Invisible bool `json:"invisible",omitempty`
|
||||
}
|
||||
|
||||
func DeviceCreate(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -114,6 +115,7 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
|
||||
"Fingerprint": fingerprint,
|
||||
"APIKey": APIKey,
|
||||
"Blocked": false,
|
||||
"Invisible": request.Invisible,
|
||||
})
|
||||
|
||||
if err3 != nil {
|
||||
@@ -185,6 +187,7 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
|
||||
"PublicHostname": request.PublicHostname,
|
||||
"Port": request.Port,
|
||||
"LighthousesList": lightHousesList,
|
||||
"Invisible": request.Invisible,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
94
src/constellation/api_devices_ping.go
Normal file
94
src/constellation/api_devices_ping.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package constellation
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
func DevicePing(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "GET" {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
|
||||
return
|
||||
}
|
||||
|
||||
if utils.LoggedInOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract device ID from URL parameters
|
||||
vars := mux.Vars(req)
|
||||
deviceID := vars["id"]
|
||||
|
||||
if deviceID == "" {
|
||||
utils.HTTPError(w, "Device ID is required", http.StatusBadRequest, "DP001")
|
||||
return
|
||||
}
|
||||
|
||||
// Connect to the collection
|
||||
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
|
||||
defer closeDb()
|
||||
if errCo != nil {
|
||||
utils.Error("Database Connect", errCo)
|
||||
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||
return
|
||||
}
|
||||
|
||||
var device utils.ConstellationDevice
|
||||
|
||||
// Find the device by DeviceName
|
||||
err := c.FindOne(nil, map[string]interface{}{
|
||||
"DeviceName": deviceID,
|
||||
}).Decode(&device)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("DevicePing: Device not found", err)
|
||||
utils.HTTPError(w, "Device not found", http.StatusNotFound, "DP002")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the device IP exists
|
||||
if device.IP == "" {
|
||||
utils.HTTPError(w, "Device has no IP address", http.StatusBadRequest, "DP003")
|
||||
return
|
||||
}
|
||||
|
||||
// Ping the device IP
|
||||
pingResult := pingIP(device.IP)
|
||||
|
||||
// Respond with the ping result
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": map[string]interface{}{
|
||||
"deviceName": device.DeviceName,
|
||||
"ip": device.IP,
|
||||
"reachable": pingResult,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func pingIP(ip string) bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// remove the CIDR suffix if present
|
||||
if len(ip) > 3 && ip[len(ip)-3] == '/' {
|
||||
ip = ip[:len(ip)-3]
|
||||
}
|
||||
|
||||
// Use platform-specific ping command
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("ping", "-n", "1", "-w", "10000", ip)
|
||||
} else {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "10", ip)
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
utils.Debug("Ping - Pinging IP "+ip+" - Reachable: \nOutput:\n"+string(output))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
92
src/constellation/api_devices_public.go
Normal file
92
src/constellation/api_devices_public.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package constellation
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// PublicDeviceInfo represents the limited device information exposed to the public API
|
||||
type PublicDeviceInfo struct {
|
||||
DeviceID string `json:"id"`
|
||||
DeviceName string `json:"name"`
|
||||
User string `json:"user"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func DevicePublicList(w http.ResponseWriter, req *http.Request) {
|
||||
// Check for GET method
|
||||
if req.Method != "GET" {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
|
||||
return
|
||||
}
|
||||
|
||||
// Get authorization header
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
http.Error(w, "Unauthorized [1]", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove "Bearer " from auth header
|
||||
auth = strings.Replace(auth, "Bearer ", "", 1)
|
||||
|
||||
// Connect to the collection
|
||||
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
|
||||
defer closeDb()
|
||||
if errCo != nil {
|
||||
utils.Error("Database Connect", errCo)
|
||||
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log("DevicePublicList: Fetching devices with API key")
|
||||
|
||||
// Find all non-blocked devices that match the API key
|
||||
cursor, err := c.Find(nil, map[string]interface{}{
|
||||
"Blocked": false,
|
||||
"Invisible": false,
|
||||
})
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("DevicePublicList: Error fetching devices", err)
|
||||
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DPL001")
|
||||
return
|
||||
}
|
||||
|
||||
var devices []utils.ConstellationDevice
|
||||
if err = cursor.All(nil, &devices); err != nil {
|
||||
utils.Error("DevicePublicList: Error decoding devices", err)
|
||||
utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DPL002")
|
||||
return
|
||||
}
|
||||
|
||||
// Always add the cosmos lighthouse device
|
||||
cosmosDevice := utils.ConstellationDevice{
|
||||
DeviceName: "cosmos",
|
||||
Nickname: "cosmos",
|
||||
IP: "192.168.201.1",
|
||||
}
|
||||
devices = append([]utils.ConstellationDevice{cosmosDevice}, devices...)
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
// Respond with the list of public device info
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": publicDevices,
|
||||
})
|
||||
}
|
||||
@@ -40,6 +40,8 @@ var NebulaFailedStarting = false
|
||||
func startNebulaInBackground() error {
|
||||
ProcessMux.Lock()
|
||||
defer ProcessMux.Unlock()
|
||||
|
||||
UpdateFirewallBlockedClients()
|
||||
|
||||
NebulaFailedStarting = false
|
||||
if process != nil {
|
||||
@@ -249,6 +251,7 @@ func ResetNebula() error {
|
||||
config.ConstellationConfig.Enabled = false
|
||||
config.ConstellationConfig.SlaveMode = false
|
||||
config.ConstellationConfig.DNSDisabled = false
|
||||
config.ConstellationConfig.FirewallBlockedClients = []string{}
|
||||
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
@@ -356,7 +359,7 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
|
||||
for _, d := range blockedDevices {
|
||||
finalConfig.PKI.Blocklist = append(finalConfig.PKI.Blocklist, d.Fingerprint)
|
||||
}
|
||||
|
||||
|
||||
finalConfig.Lighthouse.AMLighthouse = !overwriteConfig.PrivateNode
|
||||
|
||||
finalConfig.Lighthouse.Hosts = []string{}
|
||||
@@ -402,6 +405,136 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateFirewallBlockedClients() error {
|
||||
nebulaYmlPath := utils.CONFIGFOLDER + "nebula.yml"
|
||||
|
||||
// Read the existing nebula.yml file
|
||||
yamlData, err := ioutil.ReadFile(nebulaYmlPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read nebula.yml: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal the YAML data into a map
|
||||
var configMap map[string]interface{}
|
||||
err = yaml.Unmarshal(yamlData, &configMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal nebula.yml: %w", err)
|
||||
}
|
||||
|
||||
// Get the firewall configuration
|
||||
firewallMap, ok := configMap["firewall"].(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return errors.New("firewall not found in nebula.yml")
|
||||
}
|
||||
|
||||
// Get all devices from the database to map names to IPs
|
||||
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
|
||||
if errCo != nil {
|
||||
return errCo
|
||||
}
|
||||
defer closeDb()
|
||||
|
||||
var devices []utils.ConstellationDevice
|
||||
cursor, err := c.Find(nil, map[string]interface{}{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
cursor.All(nil, &devices)
|
||||
|
||||
// Always add the cosmos lighthouse device
|
||||
cosmosDevice := utils.ConstellationDevice{
|
||||
DeviceName: "cosmos",
|
||||
Nickname: "cosmos",
|
||||
IP: "192.168.201.1",
|
||||
}
|
||||
devices = append([]utils.ConstellationDevice{cosmosDevice}, devices...)
|
||||
|
||||
// Create a map of device names to IPs
|
||||
deviceIPs := make(map[string]string)
|
||||
for _, device := range devices {
|
||||
deviceIPs[device.DeviceName] = cleanIp(device.IP)
|
||||
}
|
||||
|
||||
// Get the blocked clients list from config
|
||||
blockedClients := utils.GetMainConfig().ConstellationConfig.FirewallBlockedClients
|
||||
blockedIPs := make(map[string]bool)
|
||||
for _, clientName := range blockedClients {
|
||||
if ip, exists := deviceIPs[clientName]; exists && ip != "" {
|
||||
blockedIPs[ip] = true
|
||||
utils.Log("Constellation: Blocking device " + clientName + " (" + ip + ") in firewall")
|
||||
}
|
||||
}
|
||||
|
||||
// Build new firewall rules
|
||||
newInboundRules := []interface{}{}
|
||||
newOutboundRules := []interface{}{}
|
||||
|
||||
// Always allow ICMP (ping) from any
|
||||
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
|
||||
"port": "any",
|
||||
"proto": "icmp",
|
||||
"host": "any",
|
||||
})
|
||||
|
||||
// Always allow port 4222 (NATS) from any
|
||||
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
|
||||
"port": 4222,
|
||||
"proto": "any",
|
||||
"host": "any",
|
||||
})
|
||||
|
||||
// Always allow port 53 (DNS) from any
|
||||
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
|
||||
"port": 53,
|
||||
"proto": "any",
|
||||
"host": "any",
|
||||
})
|
||||
|
||||
// Always allow lighthouse (cosmos)
|
||||
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
|
||||
"port": "any",
|
||||
"proto": "any",
|
||||
"host": "cosmos",
|
||||
})
|
||||
|
||||
// Allow outbound to any
|
||||
newOutboundRules = append(newOutboundRules, map[interface{}]interface{}{
|
||||
"port": "any",
|
||||
"proto": "any",
|
||||
"host": "any",
|
||||
})
|
||||
|
||||
for _, device := range devices {
|
||||
if device.DeviceName != "" && !blockedIPs[cleanIp(device.IP)] && device.DeviceName != "cosmos" {
|
||||
// Allow inbound from this device using its hostname
|
||||
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
|
||||
"port": "any",
|
||||
"proto": "any",
|
||||
"host": device.DeviceName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
firewallMap["inbound"] = newInboundRules
|
||||
firewallMap["outbound"] = newOutboundRules
|
||||
|
||||
// Marshal back to YAML
|
||||
updatedYaml, err := yaml.Marshal(configMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal updated config: %w", err)
|
||||
}
|
||||
|
||||
// Write back to nebula.yml
|
||||
err = ioutil.WriteFile(nebulaYmlPath, updatedYaml, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write nebula.yml: %w", err)
|
||||
}
|
||||
|
||||
utils.Log("Updated firewall rules in nebula.yml")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, device utils.ConstellationDevice, lite bool, getLicence bool) (string, error) {
|
||||
utils.Log("Exporting YAML config for " + name + " with file " + configPath)
|
||||
|
||||
@@ -525,7 +658,7 @@ func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, devi
|
||||
configHost += "/"
|
||||
}
|
||||
|
||||
configEndpoint := configHost + "cosmos/api/constellation/config-sync"
|
||||
configEndpoint := configHost
|
||||
|
||||
configHostname := strings.Split(configHost, "://")[1]
|
||||
configHostname = strings.Split(configHostname, ":")[0]
|
||||
@@ -772,6 +905,7 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, s
|
||||
"sign",
|
||||
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
|
||||
"-ca-key", utils.CONFIGFOLDER + "ca.key",
|
||||
"-subnets", "0.0.0.0/0",
|
||||
"-name", name,
|
||||
"-ip", ip,
|
||||
)
|
||||
@@ -785,6 +919,7 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, s
|
||||
"sign",
|
||||
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
|
||||
"-ca-key", utils.CONFIGFOLDER + "ca.key",
|
||||
"-subnets", "0.0.0.0/0",
|
||||
"-name", name,
|
||||
"-ip", ip,
|
||||
"-in-pub", "./temp.key",
|
||||
|
||||
@@ -423,6 +423,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||
|
||||
|
||||
// Create containers
|
||||
tempServiceList := make(map[string]ContainerCreateRequestContainer)
|
||||
for serviceName, container := range serviceRequest.Services {
|
||||
utils.Log(fmt.Sprintf("Checking service %s...", serviceName))
|
||||
OnLog(fmt.Sprintf("Checking service %s...\n", serviceName))
|
||||
@@ -491,10 +492,6 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||
OpenStdin: container.StdinOpen,
|
||||
}
|
||||
|
||||
if container.StopGracePeriod == 0 {
|
||||
containerConfig.StopTimeout = nil
|
||||
}
|
||||
|
||||
// check if there's an empty TZ env, if so, replace it with the host's TZ
|
||||
if containerConfig.Env != nil {
|
||||
for i, env := range containerConfig.Env {
|
||||
@@ -699,6 +696,18 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||
CapDrop: container.CapDrop,
|
||||
}
|
||||
|
||||
// cosmos-force-network-mode logic
|
||||
if containerConfig.Labels["cosmos-force-network-mode"] == "" {
|
||||
if (strings.HasPrefix(string(hostConfig.NetworkMode), "service:") ||
|
||||
strings.HasPrefix(string(hostConfig.NetworkMode), "container:")) {
|
||||
containerConfig.Labels["cosmos-force-network-mode"] = string(hostConfig.NetworkMode)
|
||||
}
|
||||
} else {
|
||||
hostConfig.NetworkMode = conttype.NetworkMode(containerConfig.Labels["cosmos-force-network-mode"])
|
||||
utils.Debug("Forcing network mode to " + string(hostConfig.NetworkMode))
|
||||
}
|
||||
|
||||
|
||||
if container.Runtime != "" {
|
||||
hostConfig.Runtime = strings.Join(strings.Fields(container.Runtime), " ")
|
||||
}
|
||||
@@ -903,10 +912,16 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||
// Write a response to the client
|
||||
utils.Log(fmt.Sprintf("Container %s created", container.Name))
|
||||
OnLog(fmt.Sprintf("Container %s created", container.Name))
|
||||
|
||||
tempServiceList[serviceName] = ContainerCreateRequestContainer{
|
||||
Name: container.Name,
|
||||
DependsOn: container.DependsOn,
|
||||
NetworkMode: string(hostConfig.NetworkMode),
|
||||
}
|
||||
}
|
||||
|
||||
// re-order containers dpeneding on depends_on
|
||||
startOrder, mustStart, err := ReOrderServices(serviceRequest.Services)
|
||||
startOrder, mustStart, err := ReOrderServices(tempServiceList)
|
||||
if err != nil {
|
||||
utils.Error("CreateService: Rolling back changes because of -- Container", err)
|
||||
OnLog(utils.DoErr("Rolling back changes because of -- Container creation error: "+err.Error()))
|
||||
@@ -1044,9 +1059,23 @@ func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]C
|
||||
changed := false
|
||||
|
||||
for name, service := range serviceMap {
|
||||
dependencies := service.DependsOn
|
||||
if dependencies == nil {
|
||||
dependencies = make(map[string]ContainerCreateRequestContainerDependsOnCont)
|
||||
}
|
||||
|
||||
// if network_mode is container: then we need to add a dependency
|
||||
if strings.HasPrefix(string(service.NetworkMode), "container:") {
|
||||
depService := strings.TrimPrefix(string(service.NetworkMode), "container:")
|
||||
dependencies[depService] = ContainerCreateRequestContainerDependsOnCont{
|
||||
Condition: "service_started",
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no dependencies, we can add this service to startOrder
|
||||
// Check if all dependencies are already in startOrder
|
||||
allDependenciesStarted := true
|
||||
for dependency, dependencyDetails := range service.DependsOn {
|
||||
for dependency, dependencyDetails := range dependencies {
|
||||
dependencyStarted := false
|
||||
for _, startedService := range startOrder {
|
||||
if startedService.Name == dependency {
|
||||
@@ -1086,6 +1115,13 @@ func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]C
|
||||
for name, _ := range serviceMap {
|
||||
errorMessage += "Could not start service: " + name + "\n"
|
||||
errorMessage += "Unsatisfied dependencies:\n"
|
||||
|
||||
// if network_mode is container: then we need to add a dependency
|
||||
if strings.HasPrefix(string(serviceMap[name].NetworkMode), "container:") {
|
||||
depService := strings.TrimPrefix(string(serviceMap[name].NetworkMode), "container:")
|
||||
errorMessage += depService + " (network_mode)\n"
|
||||
}
|
||||
|
||||
for dependency, _ := range serviceMap[name].DependsOn {
|
||||
_, ok := serviceMap[dependency]
|
||||
if ok {
|
||||
|
||||
@@ -125,6 +125,11 @@ func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
form.NetworkMode != "default" {
|
||||
container.Config.MacAddress = ""
|
||||
}
|
||||
// update cosmos-force-network-mode label
|
||||
if container.Config.Labels == nil {
|
||||
container.Config.Labels = make(map[string]string)
|
||||
}
|
||||
container.Config.Labels["cosmos-force-network-mode"] = form.NetworkMode
|
||||
}
|
||||
|
||||
_, err = EditContainer(container.ID, container, false)
|
||||
|
||||
@@ -130,15 +130,13 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON, noLock
|
||||
}
|
||||
}
|
||||
|
||||
if(newConfig.HostConfig.NetworkMode != "bridge" &&
|
||||
newConfig.HostConfig.NetworkMode != "default" &&
|
||||
newConfig.HostConfig.NetworkMode != "host" &&
|
||||
newConfig.HostConfig.NetworkMode != "none") {
|
||||
if(!HasLabel(newConfig, "cosmos-force-network-mode")) {
|
||||
if !HasLabel(newConfig, "cosmos-force-network-mode") {
|
||||
if (strings.HasPrefix(string(newConfig.HostConfig.NetworkMode), "service:") ||
|
||||
strings.HasPrefix(string(newConfig.HostConfig.NetworkMode), "container:")) {
|
||||
AddLabels(newConfig, map[string]string{"cosmos-force-network-mode": string(newConfig.HostConfig.NetworkMode)})
|
||||
} else {
|
||||
newConfig.HostConfig.NetworkMode = container.NetworkMode(GetLabel(newConfig, "cosmos-force-network-mode"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newConfig.HostConfig.NetworkMode = container.NetworkMode(GetLabel(newConfig, "cosmos-force-network-mode"))
|
||||
}
|
||||
|
||||
newName := newConfig.Name
|
||||
|
||||
@@ -41,7 +41,12 @@ func ExportContainer(containerID string) (ContainerCreateRequestContainer, error
|
||||
User: detailedInfo.Config.User,
|
||||
Tty: detailedInfo.Config.Tty,
|
||||
StdinOpen: detailedInfo.Config.OpenStdin,
|
||||
Hostname: detailedInfo.Config.Hostname,
|
||||
Hostname: func () string {
|
||||
if string(detailedInfo.HostConfig.NetworkMode) == "bridge" || string(detailedInfo.HostConfig.NetworkMode) == "default" {
|
||||
return detailedInfo.Config.Hostname
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
Domainname: detailedInfo.Config.Domainname,
|
||||
MacAddress: detailedInfo.NetworkSettings.MacAddress,
|
||||
NetworkMode: string(detailedInfo.HostConfig.NetworkMode),
|
||||
|
||||
@@ -494,6 +494,7 @@ func NetworkCleanUp() {
|
||||
|
||||
if(!config.DockerConfig.SkipPruneImages) {
|
||||
pruneFilters := filters.NewArgs()
|
||||
pruneFilters.Add("dangling", "false")
|
||||
report, err := DockerClient.ImagesPrune(DockerContext, pruneFilters)
|
||||
if err != nil {
|
||||
utils.Error("[DOCKER] Error pruning images", err)
|
||||
|
||||
@@ -542,6 +542,8 @@ func InitServer() *mux.Router {
|
||||
srapiAdmin.HandleFunc("/api/get-backup", configapi.BackupFileApiGet)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
|
||||
srapiAdmin.HandleFunc("/api/constellation/public-devices", constellation.DevicePublicList)
|
||||
srapiAdmin.HandleFunc("/api/constellation/devices/{id}/ping", constellation.DevicePing)
|
||||
srapiAdmin.HandleFunc("/api/constellation/restart", constellation.API_Restart)
|
||||
srapiAdmin.HandleFunc("/api/constellation/reset", constellation.API_Reset)
|
||||
srapiAdmin.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)
|
||||
|
||||
@@ -46,6 +46,13 @@ func Init() {
|
||||
Name: "cosmos-cloud",
|
||||
}
|
||||
|
||||
if config.Mpdu_ != "" && config.Mpdn_ != "" {
|
||||
defaultMarket = utils.MarketSource{
|
||||
Url: config.Mpdu_,
|
||||
Name: config.Mpdn_,
|
||||
}
|
||||
}
|
||||
|
||||
sources = append([]utils.MarketSource{defaultMarket}, sources...)
|
||||
|
||||
for _, marketDef := range sources {
|
||||
|
||||
@@ -281,9 +281,11 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
|
||||
claims["mfaDone"] = mfaDone
|
||||
claims["forDomain"] = reqHostNoPort
|
||||
|
||||
sudoUntil := time.Now().Add(time.Hour * 2).Unix()
|
||||
|
||||
// if role is ADMIN, add a timeout
|
||||
if tokenRole == utils.ADMIN {
|
||||
claims["sudo-until"] = time.Now().Add(time.Hour * 2).Unix()
|
||||
claims["sudo-until"] = sudoUntil
|
||||
}
|
||||
|
||||
key, err5 := jwt.ParseEdPrivateKeyFromPEM([]byte(utils.GetPrivateAuthKey()))
|
||||
@@ -313,7 +315,7 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
|
||||
|
||||
clientCookie := http.Cookie{
|
||||
Name: "client-infos",
|
||||
Value: user.Nickname + "," + strconv.Itoa(int(user.Role)) + "," + strconv.Itoa(int(tokenRole)),
|
||||
Value: user.Nickname + "," + strconv.Itoa(int(user.Role)) + "," + strconv.Itoa(int(tokenRole)) + "," + strconv.Itoa(int(sudoUntil - 1)),
|
||||
Expires: expiration,
|
||||
Path: "/",
|
||||
Secure: shouldCookieBeSecured(req.RemoteAddr),
|
||||
|
||||
@@ -111,6 +111,8 @@ type Config struct {
|
||||
RemoteStorage RemoteStorageConfig
|
||||
DisableOpenIDDirect bool
|
||||
Backup BackupConfig
|
||||
Mpdu_ string
|
||||
Mpdn_ string
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +299,7 @@ type ConstellationConfig struct {
|
||||
NebulaConfig NebulaConfig
|
||||
ConstellationHostname string
|
||||
Tunnels []ProxyRouteConfig
|
||||
FirewallBlockedClients []string `json:"FirewallBlockedClients" bson:"FirewallBlockedClients"`
|
||||
}
|
||||
|
||||
type ConstellationDNSEntry struct {
|
||||
@@ -317,6 +320,7 @@ type ConstellationDevice struct {
|
||||
Blocked bool `json:"blocked" bson:"Blocked"`
|
||||
Fingerprint string `json:"fingerprint" bson:"Fingerprint"`
|
||||
APIKey string `json:"-" bson:"APIKey"`
|
||||
Invisible bool `json:"invisible" bson:"Invisible"`
|
||||
}
|
||||
|
||||
type NebulaFirewallRule struct {
|
||||
|
||||
@@ -89,7 +89,7 @@ var DefaultConfig = Config{
|
||||
},
|
||||
},
|
||||
DockerConfig: DockerConfig{
|
||||
DefaultDataPath: "/usr",
|
||||
DefaultDataPath: "/cosmos-storage",
|
||||
},
|
||||
MarketConfig: MarketConfig{
|
||||
Sources: []MarketSource{
|
||||
|
||||
Reference in New Issue
Block a user