Merge branch 'unstable'

This commit is contained in:
Yann Stepienik 2023-10-07 15:54:47 +01:00
commit f796c521ef
66 changed files with 5248 additions and 1323 deletions

View file

@ -53,6 +53,24 @@ jobs:
command: |
curl -s -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAX_TOKEN&suffix=tar.gz" -o GeoLite2-Country.tar.gz
tar -xzf GeoLite2-Country.tar.gz --strip-components 1 --wildcards "*.mmdb"
- run:
name: Download and Extract ARM Nebula Binary
command: |
curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-arm64.tar.gz
tar -xzvf nebula-linux-arm64.tar.gz
- run:
name: Rename ARM Nebula Binary
command: |
mv nebula nebula-arm
mv nebula-cert nebula-cert-arm
- run:
name: Download and Extract Nebula Binary
command: |
curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-amd64.tar.gz
tar -xzvf nebula-linux-amd64.tar.gz
- run:
name: Build UI

8
.gitignore vendored
View file

@ -12,4 +12,10 @@ todo.txt
LICENCE
tokens.json
.vscode
GeoLite2-Country.mmdb
GeoLite2-Country.mmdb
dns-blacklist.txt
zz_test_config
nebula-arm
nebula-arm-cert
nebula
nebula-cert

66
LICENCE
View file

@ -1,31 +1,57 @@
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the
License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant
of rights under the License will not include, and the License
does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or
all of the rights granted to you under the License to provide
to third parties, for a fee or other consideration (including
without limitation fees for hosting or consulting/ support
services related to the Software), a product or service whose
value derives, entirely or substantially, from the functionality
of the Software. Any license notice or attribution required by
the License must also include this Commons Clause License
Condition notice.
Software: Cosmos-Server
License: Apache 2.0 with Commons Clause
License: Apache 2.0 with Commons Clause and Anti Tampering Clause
Licensor: Yann Stepienik
---------------------------------------------------------------------
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the
License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant
of rights under the License will not include, and the License
does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or
all of the rights granted to you under the License to provide
to third parties, for a fee or other consideration (including
without limitation fees for hosting or consulting/ support
services related to the Software), a product or service whose
value derives, entirely or substantially, from the functionality
of the Software. Any license notice or attribution required by
the License must also include this Commons Clause License
Condition notice.
---------------------------------------------------------------------
"Anti Tampering Clause” License Condition v1.0
Notwithstanding any provision of the Apache License 2.0, if the User
(or any party receiving or distributing derivative works, services,
or anything of value from the User related to the Software), directly
or indirectly, seeks to tamper with, alter, circumvent, or avoid
compliance with any subscription, paywall, feature restriction, or any
other licensing mechanism built into the Software or its usage, the
License granted under the Apache License 2.0 shall automatically and
immediately terminate, and access to the Software shall be withdrawn
with immediate effect. Upon such termination, any and all rights
established under the Apache License 2.0 shall be null and void.
Tampering includes but is not limited to: (a) removing, disabling,
or circumventing any license key or other copy protection mechanism,
(b) redistributing parts or all of a feature that was intended
to be a paid feature, without keeping the restrictions, limitations,
or other licensing mechanisms with it(c) disabling, circumventing, or
avoiding any feature of the Software that is intended to enforce usage or
copy restrictions, or (d) providing or distributing any information
or code that enables disabling, circumvention, or avoidance of any
feature of the Software that is intended to enforce usage or copy
restrictions.
---------------------------------------------------------------------
Apache License
Version 2.0, January 2004

View file

@ -18,6 +18,7 @@ echo " ---- Build complete, copy assets ----"
cp -r static build/
cp -r GeoLite2-Country.mmdb build/
cp nebula-arm-cert nebula-cert nebula-arm nebula build/
cp -r Logo.png build/
mkdir build/images
cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png

View file

@ -1,3 +1,19 @@
<<<<<<< HEAD
## Version 0.9.20 - 0.9.21
- Add option to disable CORS hardening (with empty value)
=======
## Version 0.10.0
- Added Constellation
- DNS Challenge is now used for all certificates when enabled [breaking change]
- Rework headers for better compatibility
- Improve experience for non-admin users
- Fix bug with redirect on logout
- Added OverwriteHostHeader to routes to override the host header sent to the target app
- Added WhitelistInboundIPs to routes to filter incoming requests based on IP per URL
> **Note: If you use the ARM (:latest-arm) you need to manually update to using the :latest tag instead**
## Version 0.9.20 - 0.9.21
- Add option to disable CORS hardening (with empty value)

2
cla.md
View file

@ -2,7 +2,7 @@ Cosmos Software Grant and Contributor License Agreement (“Agreement”)
This agreement is based on the Apache Software Foundation Contributor License Agreement. (v r190612)
Thank you for your interest in software projects stewarded by Raintank, Inc. dba Cosmos (“Cosmos”). In order to clarify the intellectual property license granted with Contributions from any person or entity, Cosmos must have a Contributor License Agreement (CLA) on file that has been agreed to by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Cosmos and its users; it does not change your rights to use your own Contributions for any other purpose. This Agreement allows an individual to contribute to Cosmos on that individuals own behalf, or an entity (the “Corporation”) to submit Contributions to Cosmos, to authorize Contributions submitted by its designated employees to Cosmos, and to grant copyright and patent licenses thereto.
Thank you for your interest in dba Cosmos (“Cosmos”). In order to clarify the intellectual property license granted with Contributions from any person or entity, Cosmos must have a Contributor License Agreement (CLA) on file that has been agreed to by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Cosmos and its users; it does not change your rights to use your own Contributions for any other purpose. This Agreement allows an individual to contribute to Cosmos on that individuals own behalf, or an entity (the “Corporation”) to submit Contributions to Cosmos, to authorize Contributions submitted by its designated employees to Cosmos, and to grant copyright and patent licenses thereto.
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Cosmos. Except for the license granted herein to Cosmos and recipients of software distributed by Cosmos, You reserve all right, title, and interest in and to Your Contributions.

View file

@ -0,0 +1,131 @@
import wrap from './wrap';
function list() {
return new Promise((resolve, reject) => {
resolve({
"data": [
{
"nickname": "admin",
"deviceName": "phone",
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\naACf/...=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
"ip": "192.168.201.4/24",
"isLighthouse": false,
"isRelay": true,
"publicHostname": "",
"port": "4242",
"blocked": false,
"fingerprint": "..."
},
{
"nickname": "admin",
"deviceName": "laptop",
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\n78l4nDEB0+.../36YBQk7dkwg+.=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
"ip": "192.168.201.5/24",
"isLighthouse": false,
"isRelay": true,
"publicHostname": "",
"port": "4242",
"blocked": false,
"fingerprint": "..."
},
{
"nickname": "Martha",
"deviceName": "pink phone",
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\naACf/..=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
"ip": "192.168.201.6/24",
"isLighthouse": false,
"isRelay": true,
"publicHostname": "",
"port": "4242",
"blocked": false,
"fingerprint": "..."
}
],
"status": "OK"
})
});
}
function addDevice(device) {
return new Promise((resolve, reject) => {
resolve({
"data": {
"CA": "-----BEGIN NEBULA CERTIFICATE-----\....\n+dfE+ikL8jUh/n+C+....\....\nZon/Dw==\n-----END NEBULA CERTIFICATE-----\n",
"Config": "constellation_api_key: ...\nconstellation_device_name: test\nconstellation_local_dns_overwrite: true\nconstellation_local_dns_overwrite_address: 192.168.201.1\nconstellation_public_hostname: \"\"\nfirewall:\n conntrack:\n default_timeout: 10m\n tcp_timeout: 12m\n udp_timeout: 3m\n inbound:\n - host: any\n port: any\n proto: any\n inbound_action: drop\n outbound:\n - host: any\n port: any\n proto: any\n outbound_action: drop\nlighthouse:\n am_lighthouse: false\n hosts:\n - 192.168.201.1\n interval: 60\nlisten:\n host: 0.0.0.0\n port: \"4242\"\nlogging:\n format: text\n level: info\npki:\n blocklist: []\n ca: |\n -----BEGIN NEBULA CERTIFICATE-----\n ...\n +dfE+ikL8jUh/n+C+...\n .\n Zon/Dw==\n -----END NEBULA CERTIFICATE-----\n cert: |\n -----BEGIN NEBULA CERTIFICATE-----\n CmIKBHRlc3QSCoeSo4UMgP7//..\n ...+QwZSiBxLdKhjkCH+.../..\n ./hfL+....\n ..==\n -----END NEBULA CERTIFICATE-----\n key: |\n -----BEGIN NEBULA X25519 PRIVATE KEY-----\n nS39dWX7uo1rhTvP2yl2XonGx3fWEkpk+43thNrMu7U=\n -----END NEBULA X25519 PRIVATE KEY-----\npunchy:\n punch: true\n respond: true\nrelay:\n am_relay: false\n relays:\n - 192.168.201.1\n use_relays: true\nstatic_host_map:\n 192.168.201.1:\n - vpn.domain.com:4242\ntun:\n dev: nebula1\n disabled: false\n drop_local_broadcast: false\n drop_multicast: false\n mtu: 1300\n routes: []\n tx_queue: 500\n unsafe_routes: []\n",
"DeviceName": "test",
"IP": "192.168.201.7/24",
"IsLighthouse": false,
"IsRelay": true,
"LighthousesList": [],
"Nickname": "admin",
"Port": "4242",
"PrivateKey": "-----BEGIN NEBULA CERTIFICATE-----\...//w8o3ZaFqQYwhdGFuAY6IGXmYRCr3z932Y....w\..==\n-----END NEBULA CERTIFICATE-----\n",
"PublicHostname": "",
"PublicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\nnS39dWX...hTvP......+43thNrMu7U=\n-----END NEBULA X25519 PRIVATE KEY-----\n"
},
"status": "OK"
})
});
}
function restart() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function reset() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function getConfig() {
return new Promise((resolve, reject) => {
resolve({
"data": "pki:\n ca: /config/ca.crt\n cert: /config/cosmos.crt\n key: /config/cosmos.key\n blocklist: []\nstatic_host_map:\n 192.168.201.1:\n - vpn.domain.com:4242\nlighthouse:\n am_lighthouse: true\n interval: 60\n hosts: []\nlisten:\n host: 0.0.0.0\n port: 4242\npunchy:\n punch: true\n respond: true\nrelay:\n am_relay: true\n use_relays: true\n relays: []\ntun:\n disabled: false\n dev: nebula1\n drop_local_broadcast: false\n drop_multicast: false\n tx_queue: 500\n mtu: 1300\n routes: []\n unsafe_routes: []\nlogging:\n level: info\n format: text\nfirewall:\n outbound_action: drop\n inbound_action: drop\n conntrack:\n tcp_timeout: 12m\n udp_timeout: 3m\n default_timeout: 10m\n outbound:\n - port: any\n proto: any\n host: any\n inbound:\n - port: any\n proto: any\n host: any\n",
"status": "OK"
})
});
}
function getLogs() {
return new Promise((resolve, reject) => {
resolve({
"data": "Some logs...",
"status": "OK"
})
});
}
function connect(file) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function block(nickname, devicename, block) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
export {
list,
addDevice,
restart,
getConfig,
getLogs,
reset,
connect,
block,
};

View file

@ -0,0 +1,110 @@
import wrap from './wrap';
function list() {
return wrap(fetch('/cosmos/api/constellation/devices', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function addDevice(device) {
return wrap(fetch('/cosmos/api/constellation/devices', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(device),
}))
}
function restart() {
return wrap(fetch('/cosmos/api/constellation/restart', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function reset() {
return wrap(fetch('/cosmos/api/constellation/reset', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function getConfig() {
return wrap(fetch('/cosmos/api/constellation/config', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function getLogs() {
return wrap(fetch('/cosmos/api/constellation/logs', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function connect(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
fetch('/cosmos/api/constellation/connect', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: reader.result,
})
.then(response => {
// Add additional response handling here if needed.
resolve(response);
})
.catch(error => {
// Handle the error.
reject(error);
});
};
reader.onerror = () => {
reject(new Error('Failed to read the file.'));
};
reader.readAsText(file);
});
}
function block(nickname, devicename, block) {
return wrap(fetch(`/cosmos/api/constellation/block`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nickname, devicename, block
}),
}))
}
export {
list,
addDevice,
restart,
getConfig,
getLogs,
reset,
connect,
block,
};

View file

@ -361,7 +361,74 @@
],
"ServerCountry": "",
"RequireMFA": false,
"AutoUpdate": false
"AutoUpdate": false,
"ConstellationConfig": {
"Enabled": true,
"SlaveMode": false,
"PrivateNode": false,
"DNSDisabled": false,
"DNSPort": "",
"DNSFallback": "8.8.8.8:53",
"DNSBlockBlacklist": true,
"DNSAdditionalBlocklists": [
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt"
],
"CustomDNSEntries": [],
"NebulaConfig": {
"PKI": {
"CA": "",
"Cert": "",
"Key": "",
"Blocklist": null
},
"StaticHostMap": null,
"Lighthouse": {
"AMLighthouse": false,
"Interval": 0,
"Hosts": null
},
"Listen": {
"Host": "",
"Port": 0
},
"Punchy": {
"Punch": false,
"Respond": false
},
"Relay": {
"AMRelay": true,
"UseRelays": false,
"Relays": null
},
"TUN": {
"Disabled": false,
"Dev": "",
"DropLocalBroadcast": false,
"DropMulticast": false,
"TxQueue": 0,
"MTU": 0,
"Routes": null,
"UnsafeRoutes": null
},
"Logging": {
"Level": "",
"Format": ""
},
"Firewall": {
"OutboundAction": "",
"InboundAction": "",
"Conntrack": {
"TCPTimeout": "",
"UDPTimeout": "",
"DefaultTimeout": ""
},
"Outbound": null,
"Inbound": null
}
},
"ConstellationHostname": "vpn.domain.com"
}
},
"updates": {
"/Sonarr": true,

View file

@ -0,0 +1,28 @@
import { Button } from "@mui/material";
export const DownloadFile = ({ filename, content, label }) => {
const downloadFile = () => {
// Create a blob with the content
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
// Create a link element
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
// Append the link to the document (needed for Firefox)
document.body.appendChild(link);
// Simulate a click to start the download
link.click();
// Cleanup the DOM by removing the link element
document.body.removeChild(link);
}
return (
<Button onClick={downloadFile}>
{label}
</Button>
);
}

View file

@ -3,6 +3,7 @@ import * as _users from './users';
import * as _config from './config';
import * as _docker from './docker';
import * as _market from './market';
import * as _constellation from './constellation';
import * as authDemo from './authentication.demo';
import * as usersDemo from './users.demo';
@ -10,6 +11,7 @@ import * as configDemo from './config.demo';
import * as dockerDemo from './docker.demo';
import * as indexDemo from './index.demo';
import * as marketDemo from './market.demo';
import * as constellationDemo from './constellation.demo';
import wrap from './wrap';
import { redirectToLocal } from '../utils/indexs';
@ -211,6 +213,7 @@ let users = _users;
let config = _config;
let docker = _docker;
let market = _market;
let constellation = _constellation;
if(isDemo) {
auth = authDemo;
@ -224,6 +227,7 @@ if(isDemo) {
checkHost = indexDemo.checkHost;
getDNS = indexDemo.getDNS;
uploadBackground = indexDemo.uploadBackground;
constellation = constellationDemo;
}
export {
@ -232,6 +236,7 @@ export {
config,
docker,
market,
constellation,
getStatus,
newInstall,
isOnline,

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -0,0 +1,94 @@
// material-ui
import { LoadingButton } from '@mui/lab';
import { Button } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { useEffect, useState } from 'react';
const preStyle = {
backgroundColor: '#000',
color: '#fff',
padding: '10px',
borderRadius: '5px',
overflow: 'auto',
maxHeight: '500px',
maxWidth: '100%',
width: '100%',
margin: '0',
position: 'relative',
fontSize: '12px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
wordBreak: 'break-all',
lineHeight: '1.5',
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
border: '1px solid rgba(255,255,255,0.1)',
boxSizing: 'border-box',
marginBottom: '10px',
marginTop: '10px',
marginLeft: '0',
marginRight: '0',
display: 'block',
textAlign: 'left',
verticalAlign: 'baseline',
opacity: '1',
}
const ApiModal = ({ callback, label }) => {
const [openModal, setOpenModal] = useState(false);
const [content, setContent] = useState("");
const [loading, setLoading] = useState(true);
const getContent = async () => {
setLoading(true);
let content = await callback();
setContent(content.data);
setLoading(false);
};
useEffect(() => {
if (openModal)
getContent();
}, [openModal]);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Refresh Page</DialogTitle>
<DialogContent>
<DialogContentText>
<pre style={preStyle}>
{content}
</pre>
</DialogContentText>
</DialogContent>
<DialogActions>
<LoadingButton
loading={loading}
onClick={() => {
getContent();
}}>Refresh</LoadingButton>
<Button onClick={() => {
setOpenModal(false);
}}>Close</Button>
</DialogActions>
</Dialog>
<Button
disableElevation
variant="outlined"
color="primary"
onClick={() => {
setOpenModal(true);
}}
>
{label}
</Button>
</>
};
export default ApiModal;

View file

@ -0,0 +1,48 @@
// material-ui
import { LoadingButton } from '@mui/lab';
import { Button } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { useEffect, useState } from 'react';
const ConfirmModal = ({ callback, label, content }) => {
const [openModal, setOpenModal] = useState(false);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText>
{content}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {
setOpenModal(false);
}}>Cancel</Button>
<LoadingButton
onClick={() => {
callback();
setOpenModal(false);
}}>Confirm</LoadingButton>
</DialogActions>
</Dialog>
<Button
disableElevation
variant="outlined"
color="warning"
onClick={() => {
setOpenModal(true);
}}
>
{label}
</Button>
</>
};
export default ConfirmModal;

View file

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from '@mui/material';
import { UploadOutlined } from '@ant-design/icons';
export default function UploadButtons({OnChange, accept, label}) {
export default function UploadButtons({OnChange, accept, label, variant, fullWidth, size}) {
return (
<div>
<input
@ -14,7 +14,8 @@ export default function UploadButtons({OnChange, accept, label}) {
onChange={OnChange}
/>
<label htmlFor="contained-button-file">
<Button variant="contained" component="span" startIcon={<UploadOutlined />}>
<Button variant={variant || "contained"} component="span"
fullWidth={fullWidth} startIcon={<UploadOutlined />}>
{label || 'Upload'}
</Button>
</label>

View file

@ -123,8 +123,9 @@
align-items: center;
}
.loading-image {
background: url('/assets/images/icons/cosmos_gray.png') no-repeat center center;
.loading-image:empty {
/* background: url('assets/images/icons/cosmos_gray.png') no-repeat center center;
background-size: contain; */
}
.raw-table table {

View file

@ -47,7 +47,7 @@ const NavGroup = ({ item }) => {
}
sx={{ mb: drawerOpen ? 1.5 : 0, py: 0, zIndex: 0 }}
>
{navCollapse}
{navCollapse}
</List>
);
};

View file

@ -9,6 +9,7 @@ import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography }
// project import
import { activeItem } from '../../../../../store/reducers/menu';
import { useClientInfos } from '../../../../../utils/hooks';
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
@ -17,6 +18,12 @@ const NavItem = ({ item, level }) => {
const dispatch = useDispatch();
const menu = useSelector((state) => state.menu);
const { drawerOpen, openItem } = menu;
const {role} = useClientInfos();
const isAdmin = role === "2";
if (item.adminOnly && !isAdmin) {
return null;
}
let itemTarget = '_self';
if (item.target) {
@ -54,6 +61,16 @@ const NavItem = ({ item, level }) => {
const textColor = 'text.primary';
const iconSelectedColor = 'primary.main';
// SET BETA (TODO REMOVE)
if(item.title === "Constellation")
item.title = <>{item.title} <span style={{
color: 'gray',
fontSize: '11px',
textDecoration: 'italic',
transform: 'translateY(-5px)',
display: 'inline-block',
}}>Beta</span></>;
return (
<ListItemButton
{...listItemProps}

View file

@ -1,5 +1,6 @@
// assets
import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
import ConstellationIcon from '../assets/images/icons/constellation.png'
// icons
const icons = {
@ -7,7 +8,6 @@ const icons = {
ProfileOutlined,
SettingOutlined
};
// ==============================|| MENU ITEMS - EXTRA PAGES ||============================== //
const pages = {
@ -20,7 +20,8 @@ const pages = {
title: 'ServApps',
type: 'item',
url: '/cosmos-ui/servapps',
icon: AppstoreOutlined
icon: AppstoreOutlined,
adminOnly: true
},
{
id: 'url',
@ -29,12 +30,21 @@ const pages = {
url: '/cosmos-ui/config-url',
icon: icons.NodeExpandOutlined,
},
{
id: 'constellation',
title: 'Constellation',
type: 'item',
url: '/cosmos-ui/constellation',
icon: () => <img height="28px" width="28px" style={{marginLeft: "-6px"}} src={ConstellationIcon} />,
},
{
id: 'users',
title: 'Users',
type: 'item',
url: '/cosmos-ui/config-users',
icon: icons.ProfileOutlined,
adminOnly: true
},
{
id: 'openid',
@ -42,6 +52,7 @@ const pages = {
type: 'item',
url: '/cosmos-ui/openid-manage',
icon: PicLeftOutlined,
adminOnly: true
},
{
id: 'config',

View file

@ -9,7 +9,7 @@ import AuthWrapper from './AuthWrapper';
import { useEffect } from 'react';
import * as API from '../../api';
import { redirectTo } from '../../utils/indexs';
import { redirectTo, redirectToLocal } from '../../utils/indexs';
// ================================|| REGISTER ||================================ //

View file

@ -11,7 +11,7 @@ import {
FormHelperText,
} from '@mui/material';
import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
import { CosmosContainerPicker } from '../users/containerPicker';
import { snackit } from '../../../api/wrap';
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
@ -72,12 +72,20 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
StripPathPrefix: routeConfig.StripPathPrefix,
AuthEnabled: routeConfig.AuthEnabled,
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
RestrictToConstellation: routeConfig.RestrictToConstellation,
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
WhitelistInboundIPs: routeConfig.WhitelistInboundIPs && routeConfig.WhitelistInboundIPs.join(', '),
}}
validationSchema={ValidateRouteSchema}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
if(!submitButton) {
return false;
} else {
let commaSepIps = values.WhitelistInboundIPs;
if(commaSepIps) {
values.WhitelistInboundIPs = commaSepIps.split(',').map((ip) => ip.trim());
}
let fullValues = {
...routeConfig,
...values,
@ -256,6 +264,37 @@ const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noC
label="Smart Shield Protection"
formik={formik}
/>
<CosmosCheckbox
name="RestrictToConstellation"
label="Restrict access to Constellation VPN"
formik={formik}
/>
<CosmosCollapse title={'Advanced Settings'}>
<Stack spacing={2}>
<Alert severity='info'>These settings are for advanced users only. Please do not change these unless you know what you are doing.</Alert>
<CosmosInputText
name="OverwriteHostHeader"
label="Overwrite Host Header (use this to chain resolve request from another server/ip)"
placeholder="Overwrite Host Header"
formik={formik}
/>
<Alert severity='warning'>
This setting will filter out all requests that do not come from the specified IPs.
This requires your setup to report the true IP of the client. By default it will, but some exotic setup (like installing docker/cosmos on Windows, or behind Cloudlfare)
will prevent Cosmos from knowing what is the client's real IP. If you used "Restrict to Constellation" above, Constellation IPs will always be allowed regardless of this setting.
</Alert>
<CosmosInputText
name="WhitelistInboundIPs"
label="Whitelist Inbound IPs and/or IP ranges (comma separated)"
placeholder="Whitelist Inbound IPs"
formik={formik}
/>
</Stack>
</CosmosCollapse>
</Grid>
</MainCard>
{submitButton && <MainCard ><Button

View file

@ -1,7 +1,7 @@
import * as React from 'react';
import MainCard from '../../../components/MainCard';
import RestartModal from '../users/restart';
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
import { Checkbox, Chip, Divider, FormControlLabel, Stack, useMediaQuery } from '@mui/material';
import HostChip from '../../../components/hostChip';
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
import { getFaviconURL } from '../../../utils/routes';
@ -9,6 +9,8 @@ import * as API from '../../../api';
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
import IsLoggedIn from '../../../isLoggedIn';
import { redirectToLocal } from '../../../utils/indexs';
import { CosmosCheckbox } from '../users/formShortcuts';
import { Field } from 'formik';
const info = {
backgroundColor: 'rgba(0, 0, 0, 0.1)',

View file

@ -31,6 +31,7 @@ import { TwitterPicker
// TODO: Remove circular deps
import {SetPrimaryColor, SetSecondaryColor} from '../../../App';
import { useClientInfos } from '../../../utils/hooks';
const ConfigManagement = () => {
const [config, setConfig] = React.useState(null);
@ -38,6 +39,8 @@ const ConfigManagement = () => {
const [openResartModal, setOpenRestartModal] = React.useState(false);
const [uploadingBackground, setUploadingBackground] = React.useState(false);
const [saveLabel, setSaveLabel] = React.useState("Save");
const {role} = useClientInfos();
const isAdmin = role === "2";
function refresh() {
API.config.get().then((res) => {
@ -62,9 +65,9 @@ const ConfigManagement = () => {
refresh();
}}>Refresh</Button>
<Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
{isAdmin && <Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
setOpenRestartModal(true);
}}>Restart Server</Button>
}}>Restart Server</Button>}
</Stack>
{config && <>
@ -186,7 +189,7 @@ const ConfigManagement = () => {
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={3}>
<MainCard>
{isAdmin && <MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
@ -205,7 +208,13 @@ const ConfigManagement = () => {
{saveLabel}
</LoadingButton>
</Grid>
</MainCard>
</MainCard>}
{!isAdmin && <div>
<Alert severity="warning">As you are not an admin, you can't edit the configuration.
This page is only here for visibility.
</Alert>
</div>}
<MainCard title="General">
<Grid container spacing={3}>
@ -331,6 +340,29 @@ const ConfigManagement = () => {
formik.setFieldValue('PrimaryColor', colorRGB);
SetPrimaryColor(colorRGB);
}}
colors={[
'#ab47bc',
'#4527a0',
'#FF6900',
'#FCB900',
'#7BDCB5',
'#00D084',
'#8ED1FC',
'#0693E3',
'#ABB8C3',
'#EB144C',
'#F78DA7',
'#9900EF',
'#FF0000',
'#FFC0CB',
'#20B2AA',
'#FFFF00',
'#8A2BE2',
'#A52A2A',
'#5F9EA0',
'#7FFF00',
'#D2691E'
]}
/>
</Stack>
</Grid>
@ -346,6 +378,29 @@ const ConfigManagement = () => {
formik.setFieldValue('SecondaryColor', colorRGB);
SetSecondaryColor(colorRGB);
}}
colors={[
'#ab47bc',
'#4527a0',
'#FF6900',
'#FCB900',
'#7BDCB5',
'#00D084',
'#8ED1FC',
'#0693E3',
'#ABB8C3',
'#EB144C',
'#F78DA7',
'#9900EF',
'#FF0000',
'#FFC0CB',
'#20B2AA',
'#FFFF00',
'#8A2BE2',
'#A52A2A',
'#5F9EA0',
'#7FFF00',
'#D2691E'
]}
/>
</Stack>
</Grid>
@ -627,7 +682,7 @@ const ConfigManagement = () => {
</Grid>
</MainCard>
<MainCard>
{isAdmin && <MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
@ -646,7 +701,7 @@ const ConfigManagement = () => {
{saveLabel}
</LoadingButton>
</Grid>
</MainCard>
</MainCard>}
</Stack>
</form>
)}

View file

@ -27,26 +27,33 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
export const CosmosInputText = ({ name, style, multiline, type, placeholder, onChange, label, formik }) => {
export const CosmosInputText = ({ name, style, value, errors, multiline, type, placeholder, onChange, label, formik }) => {
return <Grid item xs={12}>
<Stack spacing={1} style={style}>
<InputLabel htmlFor={name}>{label}</InputLabel>
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
<OutlinedInput
id={name}
type={type ? type : 'text'}
value={formik.values[name]}
value={value || (formik && formik.values[name])}
name={name}
multiline={multiline}
onBlur={formik.handleBlur}
onBlur={(...ar) => {
return formik && formik.handleBlur(...ar);
}}
onChange={(...ar) => {
onChange && onChange(...ar);
return formik.handleChange(...ar);
return formik && formik.handleChange(...ar);
}}
placeholder={placeholder}
fullWidth
error={Boolean(formik.touched[name] && formik.errors[name])}
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
/>
{formik.touched[name] && formik.errors[name] && (
{formik && formik.touched[name] && formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
)}
{errors && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
@ -206,7 +213,7 @@ export const CosmosCollapse = ({ children, title }) => {
export function CosmosFormDivider({title}) {
return <Grid item xs={12}>
<Divider>
<Chip label={title} />
{title && <Chip label={title} />}
</Divider>
</Grid>
}

View file

@ -0,0 +1,302 @@
// material-ui
import { Alert, Button, InputLabel, OutlinedInput, Stack, TextField } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { useState } from 'react';
import ResponsiveButton from '../../components/responseiveButton';
import { PlusCircleFilled } from '@ant-design/icons';
import { Formik } from 'formik';
import * as yup from 'yup';
import * as API from '../../api';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
import { DownloadFile } from '../../api/downloadButton';
import QRCode from 'qrcode';
import { useClientInfos } from '../../utils/hooks';
const getDocker = (data, isCompose) => {
let lighthouses = '';
for (let i = 0; i < data.LighthousesList.length; i++) {
const l = data.LighthousesList[i];
lighthouses += l.publicHostname + ";" + l.ip + ":" + l.port + ";" + l.isRelay + ",";
}
let containerName = "cosmos-constellation-lighthouse";
let imageName = "cosmos-constellation-lighthouse:latest";
let volPath = "/var/lib/cosmos-constellation";
if (isCompose) {
return `
version: "3.8"
services:
${containerName}:
image: ${imageName}
container_name: ${containerName}
restart: unless-stopped
network_mode: bridge
ports:
- "${data.Port}:4242"
volumes:
- ${volPath}:/config
environment:
- CA=${JSON.stringify(data.CA)}
- CERT=${JSON.stringify(data.PrivateKey)}
- KEY=${JSON.stringify(data.PublicKey)}
- LIGHTHOUSES=${lighthouses}
- PUBLIC_HOSTNAME=${data.PublicHostname}
- IS_RELAY=${data.IsRelay}
- IP=${data.IP}
`;
} else {
return `
docker run -d \\
--name ${containerName} \\
--restart unless-stopped \\
--network bridge \\
-v ${volPath}:/config \\
-e CA=${JSON.stringify(data.CA)} \\
-e CERT=${JSON.stringify(data.PrivateKey)} \\
-e KEY=${JSON.stringify(data.PublicKey)} \\
-e LIGHTHOUSES=${lighthouses} \\
-e PUBLIC_HOSTNAME=${data.PublicHostname} \\
-e IS_RELAY=${data.IsRelay} \\
-e IP=${data.IP} \\
-p ${data.Port}:4242 \\
${imageName}
`;
}
}
const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
const [openModal, setOpenModal] = useState(false);
const [isDone, setIsDone] = useState(null);
const canvasRef = React.useRef(null);
const {role, nickname} = useClientInfos();
const isAdmin = role === "2";
let firstIP = "192.168.201.2/24";
if (devices && devices.length > 0) {
const isIpFree = (ip) => {
return devices.filter((d) => d.ip === ip).length === 0;
}
let i = 1;
let j = 201;
while (!isIpFree(firstIP)) {
i++;
if (i > 254) {
i = 0;
j++;
}
firstIP = "192.168." + j + "." + i + "/24";
}
}
const renderCanvas = (data) => {
if (!canvasRef.current) return setTimeout(() => {
renderCanvas(data);
}, 500);
QRCode.toCanvas(canvasRef.current, JSON.stringify(data),
{
width: 600,
color: {
dark: "#000",
light: '#fff'
}
}, function (error) {
if (error) console.error(error)
})
}
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<Formik
initialValues={{
nickname: nickname,
deviceName: '',
ip: firstIP,
publicKey: '',
Port: "4242",
PublicHostname: '',
IsRelay: true,
isLighthouse: false,
}}
validationSchema={yup.object({
})}
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
if(values.isLighthouse) values.nickname = null;
return API.constellation.addDevice(values).then(({data}) => {
setIsDone(data);
refreshConfig();
renderCanvas(data.Config);
}).catch((err) => {
setErrors(err.response.data);
});
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<DialogTitle>Add Device</DialogTitle>
{isDone ? <DialogContent>
<DialogContentText>
<p>
Device added successfully!
Download scan the QR Code from the Cosmos app or download the relevant
files to your device along side the config and network certificate to
connect:
</p>
<Stack spacing={2} direction={"column"}>
{/* {isDone.isLighthouse ? <>
<CosmosFormDivider title={"Docker"} />
<TextField
fullWidth
multiline
value={getDocker(isDone, false)}
variant="outlined"
size="small"
disabled
/>
<CosmosFormDivider title={"File (Docker-Compose)"} />
<DownloadFile
filename={`docker-compose.yml`}
content={getDocker(isDone, true)}
label={"Download docker-compose.yml"}
/>
</> : <> */}
<CosmosFormDivider title={"QR Code"} />
<div style={{textAlign: 'center'}}>
<canvas style={{borderRadius: '15px'}} ref={canvasRef} />
</div>
{/* </>} */}
<CosmosFormDivider title={"File"} />
<DownloadFile
filename={`constellation.yml`}
content={isDone.Config}
label={"Download constellation.yml"}
/>
</Stack>
</DialogContentText>
</DialogContent> : <DialogContent>
<DialogContentText>
<p>Add a Device to the constellation using either the Cosmos or Nebula client</p>
<div>
<Stack spacing={2} style={{}}>
<CosmosCheckbox
name="isLighthouse"
label="Lighthouse"
formik={formik}
/>
{!formik.values.isLighthouse &&
(isAdmin ? <CosmosSelect
name="nickname"
label="Owner"
formik={formik}
// disabled={!isAdmin}
options={
users.map((u) => {
return [u.nickname, u.nickname]
})
}
/> : <>
<InputLabel>Owner</InputLabel>
<OutlinedInput
fullWidth
multiline
value={nickname}
variant="outlined"
size="small"
disabled
/>
</>)}
<CosmosInputText
name="deviceName"
label="Device Name"
formik={formik}
/>
<CosmosInputText
name="ip"
label="Constellation IP Address"
formik={formik}
/>
{/* <CosmosInputText
name="Port"
label="VPN Port (default: 4242)"
formik={formik}
/> */}
<CosmosInputText
multiline
name="publicKey"
label="Public Key (Optional)"
formik={formik}
/>
{formik.values.isLighthouse && <>
<CosmosFormDivider title={"Lighthouse Setup"} />
<CosmosInputText
name="PublicHostname"
label="Public Hostname"
formik={formik}
/>
<CosmosCheckbox
name="IsRelay"
label="Can Relay Traffic"
formik={formik}
/>
</>}
<div>
{formik.errors && formik.errors.length > 0 && <Stack spacing={2} direction={"column"}>
<Alert severity="error">{formik.errors.map((err) => {
return <div>{err}</div>
})}</Alert>
</Stack>}
</div>
</Stack>
</div>
</DialogContentText>
</DialogContent>}
<DialogActions>
<Button onClick={() => setOpenModal(false)}>Close</Button>
{!isDone && <Button color="primary" variant="contained" type="submit">Add</Button>}
</DialogActions>
</form>
)}
</Formik>
</Dialog>
<ResponsiveButton
color="primary"
onClick={() => {
setIsDone(null);
setOpenModal(true);
}}
variant={
"contained"
}
startIcon={<PlusCircleFilled />}
>
Add Device
</ResponsiveButton>
</>;
};
export default AddDeviceModal;

View file

@ -0,0 +1,174 @@
import React from "react";
import { useEffect, useState } from "react";
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, TabletOutlined } from "@ant-design/icons";
import IsLoggedIn from "../../isLoggedIn";
import { Alert, Button, CircularProgress, InputLabel, Stack } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
import { LoadingButton } from "@mui/lab";
import ApiModal from "../../components/apiModal";
import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
export const ConstellationDNS = () => {
const [isAdmin, setIsAdmin] = useState(false);
const [config, setConfig] = useState(null);
const refreshConfig = async () => {
let configAsync = await API.config.get();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
};
useEffect(() => {
refreshConfig();
}, []);
return <>
{(config) ? <>
<Stack spacing={2} style={{maxWidth: "1000px"}}>
<div>
<MainCard title={"Constellation Internal DNS"} content={config.constellationIP}>
<Stack spacing={2}>
<Formik
initialValues={{
Fallback: config.ConstellationConfig.DNSFallback,
DNSBlockBlacklist: config.ConstellationConfig.DNSBlockBlacklist,
DNSAdditionalBlocklists: config.ConstellationConfig.DNSAdditionalBlocklists || [],
CustomDNSEntries: config.ConstellationConfig.CustomDNSEntries || []
}}
onSubmit={(values) => {
let newConfig = { ...config };
newConfig.ConstellationConfig.DNSFallback = values.Fallback;
newConfig.ConstellationConfig.DNSBlockBlacklist = values.DNSBlockBlacklist;
newConfig.ConstellationConfig.DNSAdditionalBlocklists = values.DNSAdditionalBlocklists;
newConfig.ConstellationConfig.CustomDNSEntries = values.CustomDNSEntries;
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<Alert severity="info">This is a DNS that runs inside your Constellation network. It automatically
rewrites your domains DNS entries to be local to your network, and also allows you to do things like block ads
and trackers on all devices connected to your network. You can also add custom DNS entries to resolve to specific
IP addresses. This DNS server is only accessible from inside your network.</Alert>
<CosmosInputText formik={formik} name="Fallback" label="DNS Fallback" placeholder={'8.8.8.8:53'} />
<CosmosFormDivider title={"DNS Blocklists"} />
<CosmosCheckbox formik={formik} name="DNSBlockBlacklist" label="Use Blacklists to block domains" />
<Alert severity="warning">When changing your DNS records, always use private mode on your browser and allow some times for various caches to expire.</Alert>
<InputLabel>DNS Blocklist URLs</InputLabel>
{formik.values.DNSAdditionalBlocklists && formik.values.DNSAdditionalBlocklists.map((item, index) => (
<Stack direction={"row"} spacing={2} key={`DNSAdditionalBlocklists${item}`} width={"100%"}>
<DeleteButton onDelete={() => {
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
}} />
<div style={{flexGrow: 1}}>
<CosmosInputText
value={item}
name={`DNSAdditionalBlocklists${index}`}
placeholder={'https://example.com/blocklist.txt'}
onChange={(e) => {
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), e.target.value, ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
}}
/>
</div>
</Stack>
))}
<Stack direction="row" spacing={2}>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists, ""]);
}}>Add</Button>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("DNSAdditionalBlocklists", [
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts"
]);
}}>Reset Default</Button>
</Stack>
<CosmosFormDivider title={"DNS Custom Entries"} />
<InputLabel>DNS Custom Entries</InputLabel>
{formik.values.CustomDNSEntries && formik.values.CustomDNSEntries.map((item, index) => (
<Stack direction={"row"} spacing={2} key={`CustomDNSEntries${item}`} width={"100%"}>
<DeleteButton onDelete={() => {
formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries.slice(0, index), ...formik.values.CustomDNSEntries.slice(index + 1)]);
}} />
<div style={{flexGrow: 1}}>
<CosmosInputText
value={item.Key}
name={`CustomDNSEntries${index}-key`}
placeholder={'domain.com'}
onChange={(e) => {
const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
updatedCustomDNSEntries[index].Key = e.target.value;
formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
}}
/>
</div>
<div style={{flexGrow: 1}}>
<CosmosInputText
value={item.Value}
name={`CustomDNSEntries${index}-value`}
placeholder={'1213.123.123.123'}
onChange={(e) => {
const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
updatedCustomDNSEntries[index].Value = e.target.value;
formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
}}
/>
</div>
</Stack>
))}
<Stack direction="row" spacing={2}>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries, {
Key: "",
Value: "",
Type: "A"
}]);
}}>Add</Button>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("CustomDNSEntries", [
]);
}}>Reset</Button>
</Stack>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
Save
</LoadingButton>
</Stack>
</form>
)}
</Formik>
</Stack>
</MainCard>
</div>
</Stack>
</> : <center>
<CircularProgress color="inherit" size={20} />
</center>}
</>
};

View file

@ -0,0 +1,57 @@
import * as React from 'react';
import MainCard from '../../components/MainCard';
import { Alert, Chip, Divider, Stack, useMediaQuery } from '@mui/material';
import HostChip from '../../components/hostChip';
import { RouteMode, RouteSecurity } from '../../components/routeComponents';
import { getFaviconURL } from '../../utils/routes';
import * as API from '../../api';
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
import IsLoggedIn from '../../isLoggedIn';
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import { useClientInfos } from '../../utils/hooks';
import { ConstellationVPN } from './vpn';
import { ConstellationDNS } from './dns';
const ConstellationIndex = () => {
const {role} = useClientInfos();
const isAdmin = role === "2";
return isAdmin ? <div>
<IsLoggedIn />
<PrettyTabbedView path="/cosmos-ui/constellation/:tab" tabs={[
{
title: 'VPN',
children: <ConstellationVPN />,
path: 'vpn'
},
{
title: 'DNS',
children: <ConstellationDNS />,
path: 'dns'
},
{
title: 'Firewall',
children: <div>
<Alert severity="info">
Coming soon. This feature will allow you to open and close ports individually
on each device and decide who can access them.
</Alert>
</div>,
},
{
title: 'Unsafe Routes',
children: <div>
<Alert severity="info">
Coming soon. This feature will allow you to tunnel your traffic through
your devices to things outside of your constellation.
</Alert>
</div>,
}
]}/>
</div> : <ConstellationVPN />;
}
export default ConstellationIndex;

View file

@ -0,0 +1,219 @@
import React from "react";
import { useEffect, useState } from "react";
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, TabletOutlined } from "@ant-design/icons";
import IsLoggedIn from "../../isLoggedIn";
import { Alert, Button, CircularProgress, Stack } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
import { LoadingButton } from "@mui/lab";
import ApiModal from "../../components/apiModal";
import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
import { useClientInfos } from "../../utils/hooks";
const getDefaultConstellationHostname = (config) => {
// if domain is set, use it
if(isDomain(config.HTTPConfig.Hostname)) {
return "vpn." + config.HTTPConfig.Hostname;
} else {
return config.HTTPConfig.Hostname;
}
}
export const ConstellationVPN = () => {
const [config, setConfig] = useState(null);
const [users, setUsers] = useState(null);
const [devices, setDevices] = useState(null);
const {role} = useClientInfos();
const isAdmin = role === "2";
const refreshConfig = async () => {
let configAsync = await API.config.get();
setConfig(configAsync.data);
setDevices((await API.constellation.list()).data || []);
if(isAdmin)
setUsers((await API.users.list()).data || []);
else
setUsers([]);
};
useEffect(() => {
refreshConfig();
}, []);
const getIcon = (r) => {
if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) {
return <MobileOutlined />
}
else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) {
return <LaptopOutlined />
} else if (r.deviceName.toLowerCase().includes("desktop")) {
return <DesktopOutlined />
} else if (r.deviceName.toLowerCase().includes("tablet")) {
return <TabletOutlined />
} else if (r.deviceName.toLowerCase().includes("lighthouse") || r.deviceName.toLowerCase().includes("server")) {
return <CompassOutlined />
} else {
return <CloudOutlined />
}
}
return <>
{(devices && config && users) ? <>
<Stack spacing={2} style={{maxWidth: "1000px"}}>
<div>
<Alert severity="info">
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 <a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank">documentation</a> for more information.
In order to connect, please use the <a href="https://cosmos-cloud.io/clients" target="_blank">Constellation App</a>.
</Alert>
<MainCard title={"Constellation Setup"} content={config.constellationIP}>
<Stack spacing={2}>
{config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <>
<Alert severity="info">
You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices.
</Alert>
</>}
<Formik
initialValues={{
Enabled: config.ConstellationConfig.Enabled,
PrivateNode: config.ConstellationConfig.PrivateNode,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
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;
setTimeout(() => {
refreshConfig();
}, 1500);
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
{formik.values.Enabled && <Stack spacing={2} direction="row">
<Button
disableElevation
variant="outlined"
color="primary"
onClick={async () => {
await API.constellation.restart();
}}
>
Restart VPN Service
</Button>
<ApiModal callback={API.constellation.getLogs} label={"Show VPN logs"} />
<ApiModal callback={API.constellation.getConfig} label={"Show VPN Config"} />
<ConfirmModal
variant="outlined"
color="warning"
label={"Reset Network"}
content={"This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone."}
callback={async () => {
await API.constellation.reset();
refreshConfig();
}}
/>
</Stack>}
<CosmosCheckbox formik={formik} name="Enabled" label="Constellation Enabled" />
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
{formik.values.Enabled && <>
<CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
<CosmosCheckbox formik={formik} name="PrivateNode" label="This node is Private (no public IP)" />
{!formik.values.PrivateNode && <>
<Alert severity="info">This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. <strong>If you change this value, you will need to reset your network and reconnect all the clients!</strong></Alert>
<CosmosInputText formik={formik} name="ConstellationHostname" label="Constellation Hostname" />
</>}
</>}
</>}
<LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
Save
</LoadingButton>
<UploadButtons
accept=".yml,.yaml"
label={"Upload External Constellation Network File"}
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,
},
{
title: 'Device Name',
field: (r) => <strong>{r.deviceName}</strong>,
},
{
title: 'Owner',
field: (r) => <strong>{r.nickname}</strong>,
},
{
title: 'Type',
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
},
{
title: 'Constellation IP',
screenMin: 'md',
field: (r) => r.ip,
},
{
title: '',
clickable: true,
field: (r) => {
return <DeleteButton onDelete={async () => {
await API.constellation.block(r.nickname, r.deviceName, true);
refreshConfig();
}}></DeleteButton>
}
}
]}
/>
</>}
</Stack>
</> : <center>
<CircularProgress color="inherit" size={20} />
</center>}
</>
};

View file

@ -12,6 +12,7 @@ import { getFullOrigin } from "../../utils/routes";
import IsLoggedIn from "../../isLoggedIn";
import { ServAppIcon } from "../../utils/servapp-icon";
import Chart from 'react-apexcharts';
import { useClientInfos } from "../../utils/hooks";
export const HomeBackground = () => {
@ -87,6 +88,8 @@ const HomePage = () => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const isMd = useMediaQuery(theme.breakpoints.up('md'));
const {role} = useClientInfos();
const isAdmin = role === "2";
const blockStyle = {
margin: 0,
@ -112,9 +115,13 @@ const HomePage = () => {
}
const refreshConfig = () => {
API.docker.list().then((res) => {
setServApps(res.data);
});
if(isAdmin) {
API.docker.list().then((res) => {
setServApps(res.data);
});
} else {
setServApps([]);
}
API.config.get().then((res) => {
setConfig(res.data);
});
@ -213,47 +220,47 @@ const HomePage = () => {
<HomeBackground status={coStatus} />
<TransparentHeader />
<Stack style={{ zIndex: 2 }} spacing={1}>
{coStatus && !coStatus.database && (
{isAdmin && coStatus && !coStatus.database && (
<Alert severity="error">
No Database is setup for Cosmos! User Management and Authentication will not work.<br />
You can either setup the database, or disable user management in the configuration panel.<br />
</Alert>
)}
{coStatus && coStatus.letsencrypt && (
{isAdmin && coStatus && coStatus.letsencrypt && (
<Alert severity="error">
You have enabled Let's Encrypt for automatic HTTPS Certificate. You need to provide the configuration with an email address to use for Let's Encrypt in the configs.
</Alert>
)}
{coStatus && coStatus.LetsEncryptErrors && coStatus.LetsEncryptErrors.length > 0 && (
{isAdmin && coStatus && coStatus.LetsEncryptErrors && coStatus.LetsEncryptErrors.length > 0 && (
<Alert severity="error">
There are errors with your Let's Encrypt configuration or one of your routes, please fix them as soon as possible.:
There are errors with your Let's Encrypt configuration or one of your routes, please fix them as soon as possible:
{coStatus.LetsEncryptErrors.map((err) => {
return <div> - {err}</div>
})}
</Alert>
)}
{coStatus && coStatus.newVersionAvailable && (
{isAdmin && coStatus && coStatus.newVersionAvailable && (
<Alert severity="warning">
A new version of Cosmos is available! Please update to the latest version to get the latest features and bug fixes.
</Alert>
)}
{coStatus && coStatus.needsRestart && (
{isAdmin && coStatus && coStatus.needsRestart && (
<Alert severity="warning">
You have made changes to the configuration that require a restart to take effect. Please restart Cosmos to apply the changes.
</Alert>
)}
{coStatus && coStatus.domain && (
{isAdmin && coStatus && coStatus.domain && (
<Alert severity="error">
You are using localhost or 0.0.0.0 as a hostname in the configuration. It is recommended that you use a domain name or an IP instead.
</Alert>
)}
{coStatus && !coStatus.docker && (
{isAdmin && coStatus && !coStatus.docker && (
<Alert severity="error">
Docker is not connected! Please check your docker connection.<br />
Did you forget to add <pre>-v /var/run/docker.sock:/var/run/docker.sock</pre> to your docker run command?<br />

View file

@ -12,6 +12,7 @@ import { Link as LinkMUI } from '@mui/material'
import DockerComposeImport from '../servapps/containers/docker-compose';
import { AppstoreAddOutlined, SearchOutlined } from "@ant-design/icons";
import ResponsiveButton from "../../components/responseiveButton";
import { useClientInfos } from "../../utils/hooks";
function Screenshots({ screenshots }) {
return screenshots.length > 1 ? (
@ -23,17 +24,17 @@ function Screenshots({ screenshots }) {
: <img src={screenshots[0]} style={{ maxHeight: '300px', height: '100%', maxWidth: '100%' }} />
}
function Showcases({ showcase, isDark }) {
function Showcases({ showcase, isDark, isAdmin }) {
return (
<Carousel animation="slide" navButtonsAlwaysVisible={false} fullHeightHover="true" swipe={false}>
{
showcase.map((item, i) => <ShowcasesItem isDark={isDark} key={i} item={item} />)
showcase.map((item, i) => <ShowcasesItem isDark={isDark} key={i} item={item} isAdmin={isAdmin} />)
}
</Carousel>
)
}
function ShowcasesItem({ isDark, item }) {
function ShowcasesItem({ isDark, item, isAdmin }) {
return (
<Paper style={{
position: 'relative',
@ -68,9 +69,9 @@ function ShowcasesItem({ isDark, item }) {
overflow: 'hidden',
}}></p>
<Stack direction="row" spacing={2} justifyContent="flex-start">
<div>
{isAdmin && <div>
<DockerComposeImport installerInit defaultName={item.name} dockerComposeInit={item.compose} />
</div>
</div>}
<Link to={"/cosmos-ui/market-listing/cosmos-cloud/" + item.name} style={{
textDecoration: 'none',
}}>
@ -110,6 +111,8 @@ const MarketPage = () => {
const isDark = theme.palette.mode === 'dark';
const { appName, appStore } = useParams();
const [search, setSearch] = useState("");
const {role} = useClientInfos();
const isAdmin = role === "2";
const backgroundStyle = isDark ? {
backgroundColor: 'rgb(0,0,0)',
@ -178,7 +181,7 @@ const MarketPage = () => {
</Link>
<div style={{ textAlign: 'center' }}>
<Screenshots screenshots={openedApp.screenshots} />
<Screenshots screenshots={openedApp.screenshots} isAdmin={isAdmin}/>
</div>
<Stack direction="row" spacing={2}>
@ -202,9 +205,9 @@ const MarketPage = () => {
<div dangerouslySetInnerHTML={{ __html: openedApp.longDescription }}></div>
<div>
{isAdmin && <div>
<DockerComposeImport installerInit defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />
</div>
</div>}
</Stack>
</Stack>
</Box>}
@ -223,7 +226,7 @@ const MarketPage = () => {
size={100}
/>
</Box>}
{showcase && showcase.length > 0 && <Showcases showcase={showcase} isDark={isDark} />}
{showcase && showcase.length > 0 && <Showcases showcase={showcase} isDark={isDark} isAdmin={isAdmin} />}
</Stack>
<Stack spacing={1} style={{

View file

@ -15,6 +15,7 @@ import ContainerIndex from '../pages/servapps/containers';
import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm';
import OpenIdList from '../pages/openid/openid-list';
import MarketPage from '../pages/market/listing';
import ConstellationIndex from '../pages/constellation';
// render - dashboard
@ -44,6 +45,10 @@ const MainRoutes = {
path: '/cosmos-ui/dashboard',
element: <DashboardDefault />
},
{
path: '/cosmos-ui/constellation',
element: <ConstellationIndex />
},
{
path: '/cosmos-ui/servapps',
element: <ServAppsIndex />

29
client/src/utils/hooks.js Normal file
View file

@ -0,0 +1,29 @@
import React from 'react';
import { useCookies } from 'react-cookie';
import { logout } from '../api/authentication';
function useClientInfos() {
const [cookies] = useCookies(['client-infos']);
let clientInfos = null;
try {
// Try to parse the cookie into a JavaScript object
clientInfos = cookies['client-infos'].split(',');
return {
nickname: clientInfos[0],
role: clientInfos[1]
};
} catch (error) {
console.error('Error parsing client-infos cookie:', error);
return {
nickname: "",
role: 2
};
}
}
export {
useClientInfos
};

View file

@ -1,3 +1,5 @@
import { Button } from "@mui/material";
export const randomString = (length) => {
let text = "";
const possible =
@ -45,4 +47,4 @@ export const redirectToLocal = (url) => {
throw new Error("URL must be local");
}
window.location.href = url;
}
}

View file

@ -29,7 +29,7 @@ WORKDIR /app
COPY build/cosmos build/cosmos-arm64 ./
# Copy other resources
COPY build/cosmos_gray.png build/Logo.png build/GeoLite2-Country.mmdb build/meta.json ./
COPY build/* ./
COPY static ./static
# Run the respective binary based on the BINARY_NAME

View file

@ -13,7 +13,8 @@ RUN apt-get update \
WORKDIR /app
COPY build/cosmos build/cosmos_gray.png build/Logo.png build/GeoLite2-Country.mmdb build/meta.json ./
COPY build/* ./
COPY static ./static
CMD ["./cosmos"]

27
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/docker/docker v23.0.3+incompatible
github.com/docker/go-connections v0.4.0
github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9
github.com/go-acme/lego/v4 v4.13.3
github.com/go-acme/lego/v4 v4.14.2
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/httprate v0.7.1
github.com/go-playground/validator/v10 v10.14.0
@ -16,7 +16,9 @@ require (
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/jasonlvhit/gocron v0.0.1
github.com/miekg/dns v1.1.55
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/ory/fosite v0.44.0
github.com/oschwald/geoip2-golang v1.8.0
github.com/pquerna/otp v1.4.0
@ -27,6 +29,7 @@ require (
golang.org/x/crypto v0.10.0
golang.org/x/net v0.11.0
golang.org/x/sys v0.9.0
gopkg.in/yaml.v2 v2.4.0
)
require (
@ -57,7 +60,20 @@ require (
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/andybalholm/cascadia v1.1.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/aws/aws-sdk-go v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.28 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@ -126,7 +142,6 @@ require (
github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/goveralls v0.0.6 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -137,6 +152,7 @@ require (
github.com/montanaflynn/stats v0.7.0 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
github.com/nrdcg/desec v0.7.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect
@ -153,7 +169,7 @@ require (
github.com/ory/viper v1.7.5 // indirect
github.com/ory/x v0.0.214 // indirect
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
github.com/ovh/go-ovh v1.4.1 // indirect
github.com/ovh/go-ovh v1.4.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pborman/uuid v1.2.0 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
@ -167,7 +183,6 @@ require (
github.com/sacloud/packages-go v0.0.9 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.2 // indirect
@ -210,9 +225,9 @@ require (
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.6 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.4.0 // indirect
)

48
go.sum
View file

@ -60,6 +60,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@ -107,9 +108,35 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo=
github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k=
github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw=
github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A=
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA=
github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 h1:hMUCiE3Zi5AHrRNGf5j985u0WyqI6r2NULhUfo0N/No=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU6Cb1H0U/TLEcYYp66mYqsPpcc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 h1:PwNeYoonBzmTdCztKiiutws3U24KrnDBuabzRfIlZY4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco=
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 h1:p4mTxJfCAyiTT4Wp6p/mOPa6j5MqCSRGot8qZwFs+Z0=
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 h1:e5mnydVdCVWxP+5rPAGi2PYxC7u2OZgH1ypC114H04U=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY=
github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@ -259,8 +286,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-acme/lego/v4 v4.13.3 h1:aZ1S9FXIkCWG3Uw/rZKSD+MOuO8ZB1t6p9VCg6jJiNY=
github.com/go-acme/lego/v4 v4.13.3/go.mod h1:c/iodVGMeBXG/+KiQczoNkySo3YLWTVa0kiyeVd/FHc=
github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHGM=
github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c=
github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
@ -672,6 +699,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
@ -1024,11 +1052,15 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo=
github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 h1:qpB3wZR4+MPK92cTC9zZPnndkJgDgPvQqPUAgVc1NXU=
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9/go.mod h1:HUoHXDrFvidN1NK9Wb/mZKNOfDNutKkzF2Pg71M9hHA=
github.com/nrdcg/desec v0.7.0 h1:iuGhi4pstF3+vJWwt292Oqe2+AsSPKDynQna/eu1fDs=
github.com/nrdcg/desec v0.7.0/go.mod h1:e1uRqqKv1mJdd5+SQROAhmy75lKMphLzWIuASLkpeFY=
github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
@ -1124,8 +1156,8 @@ github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
github.com/ovh/go-ovh v1.4.1 h1:VBGa5wMyQtTP7Zb+w97zRCh9sLtM/2YKRyy+MEJmWaM=
github.com/ovh/go-ovh v1.4.1/go.mod h1:6bL6pPyUT7tBfI0pqOegJgRjgjuO+mOo+MyXd1EEC0M=
github.com/ovh/go-ovh v1.4.2 h1:ub4jVK6ERbiBTo4y5wbLCjeKCjGY+K36e7BviW+MaAU=
github.com/ovh/go-ovh v1.4.2/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@ -1241,8 +1273,6 @@ github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UD
github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 h1:ZTzdx88+AcnjqUfJwnz89UBrMSBQ1NEysg9u5d+dU9c=
github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
@ -1877,6 +1907,8 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/ns1/ns1-go.v2 v2.7.6 h1:mCPl7q0jbIGACXvGBljAuuApmKZo3rRi4tlRIEbMvjA=
gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

32
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cosmos-server",
"version": "0.8.3",
"version": "0.10.0-unstable16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cosmos-server",
"version": "0.8.3",
"version": "0.10.0-unstable16",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0",
@ -36,6 +36,7 @@
"react": "^18.2.0",
"react-apexcharts": "^1.4.0",
"react-color": "^2.19.3",
"react-cookie": "^6.1.1",
"react-copy-to-clipboard": "^5.1.0",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
@ -3708,6 +3709,11 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz",
"integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q=="
},
"node_modules/@types/cookie": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz",
"integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA=="
},
"node_modules/@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
@ -8900,6 +8906,19 @@
"react": "*"
}
},
"node_modules/react-cookie": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-6.1.1.tgz",
"integrity": "sha512-fuFRpf8LH6SfmVMowDUIRywJF5jAUDUWrm0EI5VdXfTl5bPcJ7B0zWbuYpT0Tvikx7Gs18MlvAT+P+744dUz2g==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.1",
"hoist-non-react-statics": "^3.3.2",
"universal-cookie": "^6.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
@ -10368,6 +10387,15 @@
"node": ">=4"
}
},
"node_modules/universal-cookie": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-6.1.1.tgz",
"integrity": "sha512-33S9x3CpdUnnjwTNs2Fgc41WGve2tdLtvaK2kPSbZRc5pGpz2vQFbRWMxlATsxNNe/Cy8SzmnmbuBM85jpZPtA==",
"dependencies": {
"@types/cookie": "^0.5.1",
"cookie": "^0.5.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.9.21",
"version": "0.10.0-unstable31",
"description": "",
"main": "test-server.js",
"bugs": {
@ -36,6 +36,7 @@
"react": "^18.2.0",
"react-apexcharts": "^1.4.0",
"react-color": "^2.19.3",
"react-cookie": "^6.1.1",
"react-copy-to-clipboard": "^5.1.0",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
@ -63,13 +64,12 @@
"scripts": {
"client": "vite",
"client-build": "vite build --base=/cosmos-ui/",
"start": "env CONFIG_FILE=./config_dev.json EZ=UTC ACME_STAGING=true build/cosmos",
"start": "env COSMOS_CONFIG_FOLDER=/mnt/e/work/Cosmos-Server/zz_test_config/ CONFIG_FILE=./config_dev.json EZ=UTC ACME_STAGING=true build/cosmos",
"build": "sh build.sh",
"dev": "npm run build && npm run start",
"dockerdevbuild": "sh build.sh && docker build -f dockerfile.local --tag cosmos-dev .",
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 7200:443 -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
"dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
"dockerdevclient": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run --cap-add NET_ADMIN -d -p 7200:443 -p 80:80 -p 53:53 -p 443:443 -p 4242:4242 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
"dockerdev": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun",
"demo": "vite build --base=/cosmos-ui/ --mode demo",
"devdemo": "vite --mode demo"
},

View file

@ -7,8 +7,7 @@
<p align="center"><a href="https://github.com/DrMxrcy"><img src="https://avatars.githubusercontent.com/DrMxrcy" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
<a href="https://github.com/soldier1"><img src="https://avatars.githubusercontent.com/soldier1" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
<a href="https://github.com/devcircus"><img src="https://avatars.githubusercontent.com/devcircus" style="border-radius:48px" width="48" height="48" alt="Clayton Stone" title="Clayton Stone" /></a>
<a href="https://github.com/BillyDas"><img src="https://avatars.githubusercontent.com/BillyDas" style="border-radius:48px" width="48" height="48" alt="Billy Das" title="Billy Das" /></a>
<a href="https://github.com/Serph91P"><img src="https://avatars.githubusercontent.com/Serph91P" style="border-radius:48px" width="48" height="48" alt="Seraph91P" title="Seraph91P" /></a>
<a href="https://github.com/BlackrazorNZ"><img src="https://avatars.githubusercontent.com/BlackrazorNZ" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
</p><!-- /sponsors -->
---
@ -45,6 +44,7 @@ Cosmos is a:
* **Reverse-Proxy** 🔄🔗 Targeting containers, other servers, or serving static folders / SPA with **automatic HTTPS**, and a **nice UI**
* **Authentication Server** 👦👩 With strong security, **multi-factor authentication** and multiple strategies (**OpenId**, forward headers, HTML)
* **Container manager** 🐋🔧 To easily manage your containers and their settings, keep them up to date as well as audit their security. Includes docker-compose support!
* **VPN** 🌐🔒 To securely access your applications from anywhere, without having to open ports on your router.
* **Identity Provider** 👦👩 To easily manage your users, **invite your friends and family** to your applications without awkardly sharing credentials. Let them request a password change with an email rather than having you unlock their account manually!
* **SmartShield technology** 🧠🛡 Automatically secure your applications without manual adjustments (see below for more details). Includes anti-bot and anti-DDOS strategies.
@ -146,15 +146,17 @@ Note that **you are allowed** to use it to host a monetized business website, a
Installation is simple using Docker:
```
docker run -d -p 80:80 -p 443:443 --privileged --name cosmos-server -h cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /:/mnt/host -v /var/lib/cosmos:/config azukaar/cosmos-server:latest
docker run -d -p 80:80 -p 443:443 -p 4242:4242/udp --privileged --name cosmos-server -h cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /:/mnt/host -v /var/lib/cosmos:/config azukaar/cosmos-server:latest
```
in this command, `-v /:/mnt/host` is optional and allow to manage folders from Cosmos, you can remove it if you don't want it but you will have to create your container's bind folders manually.
`--privileged` is also optional, but it is required if you use hardening software like AppArmor or SELinux, as they restrict access to the docker socket.
`--privileged` is also optional, but it is required if you use hardening software like AppArmor or SELinux, as they restrict access to the docker socket. It is also required for Constellation to work. If you don't want to use it, you can add the following capabilities: NET_ADMIN for Constellation.
Once installed, simply go to `http://your-server-ip` and follow the instructions of the setup wizard.
Port 4242 is a UDP port used for the Constellation VPN.
make sure you expose the right ports (by default 80 / 443). It is best to keep those ports intacts, as Cosmos is meant to run as your reverse proxy. Trying to setup Cosmos behind another reverse proxy is possible but will only create headaches.
You also need to keep the docker socket mounted, as Cosmos needs to be able to manage your containers.

View file

@ -54,7 +54,7 @@ func UploadBackground(w http.ResponseWriter, req *http.Request) {
}
// create a new file in the config directory
dst, err := os.Create("/config/background" + ext)
dst, err := os.Create(utils.CONFIGFOLDER + "background" + ext)
if err != nil {
utils.HTTPError(w, "Error creating destination file", http.StatusInternalServerError, "FILE004")
return
@ -99,7 +99,7 @@ func GetBackground(w http.ResponseWriter, req *http.Request) {
if(req.Method == "GET") {
// get the background image
bg, err := ioutil.ReadFile("/config/background." + ext)
bg, err := ioutil.ReadFile(utils.CONFIGFOLDER + "background." + ext)
if err != nil {
utils.HTTPError(w, "Error reading background image", http.StatusInternalServerError, "FILE003")
return

View file

@ -42,6 +42,7 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
"data": config,
"updates": utils.UpdateAvailable,
"hostname": os.Getenv("HOSTNAME"),
"isAdmin": isAdmin,
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/authorizationserver"
"github.com/azukaar/cosmos-server/src/constellation"
)
func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
@ -43,6 +44,7 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
utils.DisconnectDB()
authorizationserver.Init()
utils.RestartHTTPServer()
constellation.RestartNebula()
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",

190
src/constellation/DNS.go Normal file
View file

@ -0,0 +1,190 @@
package constellation
import (
"time"
"strconv"
"strings"
"io/ioutil"
"github.com/miekg/dns"
"github.com/azukaar/cosmos-server/src/utils"
)
var DNSBlacklist = map[string]bool{}
func externalLookup(client *dns.Client, r *dns.Msg, serverAddr string) (*dns.Msg, time.Duration, error) {
rCopy := r.Copy() // Create a copy of the request to forward
rCopy.Id = dns.Id() // Assign a new ID for the forwarded request
// Enable DNSSEC
rCopy.SetEdns0(4096, true)
rCopy.CheckingDisabled = false
rCopy.MsgHdr.AuthenticatedData = true
return client.Exchange(rCopy, serverAddr)
}
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
config := utils.GetMainConfig()
DNSFallback := config.ConstellationConfig.DNSFallback
if DNSFallback == "" {
DNSFallback = "8.8.8.8:53"
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
customHandled := false
// []string hostnames
hostnames := utils.GetAllHostnames(false, true)
if !customHandled {
customDNSEntries := config.ConstellationConfig.CustomDNSEntries
// Overwrite local hostnames with custom entries
for _, q := range r.Question {
for _, entry := range customDNSEntries {
hostname := entry.Key
ip := entry.Value
if strings.HasSuffix(q.Name, hostname + ".") && q.Qtype == dns.TypeA {
utils.Debug("DNS Overwrite " + hostname + " with " + ip)
rr, _ := dns.NewRR(q.Name + " A " + ip)
m.Answer = append(m.Answer, rr)
customHandled = true
}
}
}
}
if !customHandled {
// Overwrite local hostnames with Constellation IP
for _, q := range r.Question {
utils.Debug("DNS Question " + q.Name)
for _, hostname := range hostnames {
if strings.HasSuffix(q.Name, hostname + ".") && q.Qtype == dns.TypeA {
utils.Debug("DNS Overwrite " + hostname + " with 192.168.201.1")
rr, _ := dns.NewRR(q.Name + " A 192.168.201.1")
m.Answer = append(m.Answer, rr)
customHandled = true
}
}
}
}
if !customHandled {
// Block blacklisted domains
for _, q := range r.Question {
noDot := strings.TrimSuffix(q.Name, ".")
if DNSBlacklist[noDot] {
if q.Qtype == dns.TypeA {
utils.Debug("DNS Block " + noDot)
rr, _ := dns.NewRR(q.Name + " A 0.0.0.0")
m.Answer = append(m.Answer, rr)
}
customHandled = true
}
}
}
// If not custom handled, use external DNS
if !customHandled {
client := new(dns.Client)
externalResponse, time, err := externalLookup(client, r, DNSFallback)
if err != nil {
utils.Error("Failed to forward query:", err)
return
}
utils.Debug("DNS Forwarded DNS query to "+DNSFallback+" in " + time.String())
externalResponse.Id = r.Id
m = externalResponse
}
w.WriteMsg(m)
}
func isDomain(domain string) bool {
// contains . and at least a letter and no special characters invalid in a domain
if strings.Contains(domain, ".") && strings.ContainsAny(domain, "abcdefghijklmnopqrstuvwxyz") && !strings.ContainsAny(domain, " !@#$%^&*()+=[]{}\\|;:'\",/<>?") {
return true
}
return false
}
func loadRawBlockList(DNSBlacklistRaw string) {
DNSBlacklistArray := strings.Split(string(DNSBlacklistRaw), "\n")
for _, domain := range DNSBlacklistArray {
if domain != "" && !strings.HasPrefix(domain, "#") {
splitDomain := strings.Split(domain, " ")
if len(splitDomain) == 1 && isDomain(splitDomain[0]) {
DNSBlacklist[splitDomain[0]] = true
} else if len(splitDomain) == 2 {
if isDomain(splitDomain[0]) {
DNSBlacklist[splitDomain[0]] = true
} else if isDomain(splitDomain[1]) {
DNSBlacklist[splitDomain[1]] = true
}
}
}
}
}
func InitDNS() {
config := utils.GetMainConfig()
DNSPort := config.ConstellationConfig.DNSPort
DNSBlockBlacklist := config.ConstellationConfig.DNSBlockBlacklist
if DNSPort == "" {
DNSPort = "53"
}
if DNSBlockBlacklist {
DNSBlacklist = map[string]bool{}
blacklistPath := utils.CONFIGFOLDER + "dns-blacklist.txt"
utils.Log("Loading DNS blacklist from " + blacklistPath)
fileExist := utils.FileExists(blacklistPath)
if fileExist {
DNSBlacklistRaw, err := ioutil.ReadFile(blacklistPath)
if err != nil {
utils.Error("Failed to load DNS blacklist", err)
} else {
loadRawBlockList(string(DNSBlacklistRaw))
}
} else {
utils.Log("No DNS blacklist found")
}
// download additional blocklists from config.DNSAdditionalBlocklists []string
for _, url := range config.ConstellationConfig.DNSAdditionalBlocklists {
utils.Log("Downloading DNS blacklist from " + url)
DNSBlacklistRaw, err := utils.DownloadFile(url)
if err != nil {
utils.Error("Failed to download DNS blacklist", err)
} else {
loadRawBlockList(DNSBlacklistRaw)
}
}
utils.Log("Loaded " + strconv.Itoa(len(DNSBlacklist)) + " domains")
}
if(!config.ConstellationConfig.DNSDisabled) {
go (func() {
dns.HandleFunc(".", handleDNSRequest)
server := &dns.Server{Addr: ":" + DNSPort, Net: "udp"}
utils.Log("Starting DNS server on :" + DNSPort)
if err := server.ListenAndServe(); err != nil {
utils.Fatal("Failed to start server: %s\n", err)
}
})()
}
}

View file

@ -0,0 +1,101 @@
package constellation
import (
"net/http"
"encoding/json"
"github.com/azukaar/cosmos-server/src/utils"
)
type DeviceBlockRequestJSON struct {
Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"`
DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
Block bool `json:"block",omitempty`
}
func DeviceBlock(w http.ResponseWriter, req *http.Request) {
if(req.Method == "POST") {
var request DeviceBlockRequestJSON
err1 := json.NewDecoder(req.Body).Decode(&request)
if err1 != nil {
utils.Error("ConstellationDeviceBlocking: Invalid User Request", err1)
utils.HTTPError(w, "Device Creation Error",
http.StatusInternalServerError, "DB001")
return
}
errV := utils.Validate.Struct(request)
if errV != nil {
utils.Error("DeviceBlocking: Invalid User Request", errV)
utils.HTTPError(w, "Device Creation Error: " + errV.Error(),
http.StatusInternalServerError, "DB002")
return
}
nickname := utils.Sanitize(request.Nickname)
deviceName := utils.Sanitize(request.DeviceName)
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return
}
utils.Log("ConstellationDeviceBlocking: Blocking Device " + deviceName)
c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices")
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
return
}
device := utils.Device{}
utils.Debug("ConstellationDeviceBlocking: Blocking Device " + deviceName)
err2 := c.FindOne(nil, map[string]interface{}{
"DeviceName": deviceName,
"Nickname": nickname,
"Blocked": false,
}).Decode(&device)
if err2 == nil {
utils.Debug("ConstellationDeviceBlocking: Found Device " + deviceName)
_, err3 := c.UpdateOne(nil, map[string]interface{}{
"DeviceName": deviceName,
"Nickname": nickname,
}, map[string]interface{}{
"$set": map[string]interface{}{
"Blocked": request.Block,
},
})
if err3 != nil {
utils.Error("DeviceBlocking: Error while updating device", err3)
utils.HTTPError(w, "Device Creation Error: " + err3.Error(),
http.StatusInternalServerError, "DB001")
return
}
if request.Block {
utils.Log("ConstellationDeviceBlocking: Device " + deviceName + " blocked")
} else {
utils.Log("ConstellationDeviceBlocking: Device " + deviceName + " unblocked")
}
} else {
utils.Error("DeviceBlocking: Error while finding device", err2)
utils.HTTPError(w, "Device Creation Error: " + err2.Error(),
http.StatusInternalServerError, "DB001")
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
} else {
utils.Error("DeviceBlocking: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -0,0 +1,41 @@
package constellation
// import (
// "net/http"
// "encoding/json"
// "math/rand"
// "time"
// "net"
// "github.com/azukaar/cosmos-server/src/utils"
// )
// func DeviceConfig(w http.ResponseWriter, req *http.Request) {
// time.Sleep(time.Duration(rand.Float64()*2)*time.Second)
// if(req.Method == "GET") {
// ip, _, err := net.SplitHostPort(req.RemoteAddr)
// if err != nil {
// http.Error(w, "Invalid request", http.StatusBadRequest)
// return
// }
// // get authorization header
// auth := req.Header.Get("Authorization")
// if auth == "" {
// http.Error(w, "Unauthorized", http.StatusUnauthorized)
// return
// }
// // remove "Bearer " from auth header
// auth = strings.Replace(auth, "Bearer ", "", 1)
// } else {
// utils.Error("DeviceConfig: Method not allowed" + req.Method, nil)
// utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
// return
// }
// }

View file

@ -0,0 +1,182 @@
package constellation
import (
"net/http"
"encoding/json"
"go.mongodb.org/mongo-driver/mongo"
"github.com/azukaar/cosmos-server/src/utils"
)
type DeviceCreateRequestJSON struct {
DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
IP string `json:"ip",validate:"required,ipv4"`
PublicKey string `json:"publicKey",omitempty`
// for devices only
Nickname string `json:"nickname",validate:"max=32,alphanum",omitempty`
// for lighthouse only
IsLighthouse bool `json:"isLighthouse",omitempty`
IsRelay bool `json:"isRelay",omitempty`
PublicHostname string `json:"PublicHostname",omitempty`
Port string `json:"port",omitempty`
}
func DeviceCreate(w http.ResponseWriter, req *http.Request) {
if(req.Method == "POST") {
var request DeviceCreateRequestJSON
err1 := json.NewDecoder(req.Body).Decode(&request)
if err1 != nil {
utils.Error("ConstellationDeviceCreation: Invalid User Request", err1)
utils.HTTPError(w, "Device Creation Error",
http.StatusInternalServerError, "DC001")
return
}
errV := utils.Validate.Struct(request)
if errV != nil {
utils.Error("DeviceCreation: Invalid User Request", errV)
utils.HTTPError(w, "Device Creation Error: " + errV.Error(),
http.StatusInternalServerError, "DC002")
return
}
nickname := utils.Sanitize(request.Nickname)
deviceName := utils.Sanitize(request.DeviceName)
APIKey := utils.GenerateRandomString(32)
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return
}
utils.Log("ConstellationDeviceCreation: Creating Device " + deviceName)
c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices")
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
return
}
device := utils.Device{}
utils.Debug("ConstellationDeviceCreation: Creating Device " + deviceName)
err2 := c.FindOne(nil, map[string]interface{}{
"DeviceName": deviceName,
"Blocked": false,
}).Decode(&device)
if err2 == mongo.ErrNoDocuments {
cert, key, fingerprint, err := generateNebulaCert(deviceName, request.IP, request.PublicKey, false)
if err != nil {
utils.Error("DeviceCreation: Error while creating Device", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC001")
return
}
if request.IsLighthouse && request.Nickname != "" {
utils.Error("DeviceCreation: Lighthouse cannot belong to a user", nil)
utils.HTTPError(w, "Device Creation Error: Lighthouse cannot have a nickname",
http.StatusInternalServerError, "DC003")
return
}
if err != nil {
utils.Error("DeviceCreation: Error while getting fingerprint", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC007")
return
}
_, err3 := c.InsertOne(nil, map[string]interface{}{
"Nickname": nickname,
"DeviceName": deviceName,
"PublicKey": key,
"IP": request.IP,
"IsLighthouse": request.IsLighthouse,
"IsRelay": request.IsRelay,
"PublicHostname": request.PublicHostname,
"Port": request.Port,
"Fingerprint": fingerprint,
"APIKey": APIKey,
"Blocked": false,
})
if err3 != nil {
utils.Error("DeviceCreation: Error while creating Device", err3)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC004")
return
}
capki, err := getCApki()
if err != nil {
utils.Error("DeviceCreation: Error while reading ca.crt", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC006")
return
}
lightHousesList := []utils.ConstellationDevice{}
if request.IsLighthouse {
lightHousesList, err = GetAllLightHouses()
}
// read configYml from config/nebula.yml
configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key, APIKey, utils.ConstellationDevice{
Nickname: nickname,
DeviceName: deviceName,
PublicKey: key,
IP: request.IP,
IsLighthouse: request.IsLighthouse,
IsRelay: request.IsRelay,
PublicHostname: request.PublicHostname,
Port: request.Port,
APIKey: APIKey,
})
if err != nil {
utils.Error("DeviceCreation: Error while reading config", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC005")
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{
"Nickname": nickname,
"DeviceName": deviceName,
"PublicKey": key,
"PrivateKey": cert,
"IP": request.IP,
"Config": configYml,
"CA": capki,
"IsLighthouse": request.IsLighthouse,
"IsRelay": request.IsRelay,
"PublicHostname": request.PublicHostname,
"Port": request.Port,
"LighthousesList": lightHousesList,
},
})
} else if err2 == nil {
utils.Error("DeviceCreation: Device already exists", nil)
utils.HTTPError(w, "Device name already exists", http.StatusConflict, "DC002")
return
} else {
utils.Error("DeviceCreation: Error while finding device", err2)
utils.HTTPError(w, "Device Creation Error: " + err2.Error(),
http.StatusInternalServerError, "DC001")
return
}
} else {
utils.Error("DeviceCreation: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -0,0 +1,18 @@
package constellation
import (
"net/http"
"github.com/azukaar/cosmos-server/src/utils"
)
func ConstellationAPIDevices(w http.ResponseWriter, req *http.Request) {
if (req.Method == "GET") {
DeviceList(w, req)
} else if (req.Method == "POST") {
DeviceCreate(w, req)
} else {
utils.Error("UserRoute: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -0,0 +1,73 @@
package constellation
import (
"net/http"
"encoding/json"
"github.com/azukaar/cosmos-server/src/utils"
)
func DeviceList(w http.ResponseWriter, req *http.Request) {
// Check for GET method
if req.Method != "GET" {
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
return
}
if utils.LoggedInOnly(w, req) != nil {
return
}
isAdmin := utils.IsAdmin(req)
// Connect to the collection
c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices")
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
return
}
var devices []utils.ConstellationDevice
// Check if user is an admin
if isAdmin {
// If admin, get all devices
cursor, err := c.Find(nil, map[string]interface{}{})
if err != nil {
utils.Error("DeviceList: Error fetching devices", err)
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DL001")
return
}
defer cursor.Close(nil)
if err = cursor.All(nil, &devices); err != nil {
utils.Error("DeviceList: Error decoding devices", err)
utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DL002")
return
}
} else {
// If not admin, get user's devices based on their nickname
nickname := req.Header.Get("x-cosmos-user")
cursor, err := c.Find(nil, map[string]interface{}{"Nickname": nickname})
if err != nil {
utils.Error("DeviceList: Error fetching devices", err)
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DL003")
return
}
defer cursor.Close(nil)
if err = cursor.All(nil, &devices); err != nil {
utils.Error("DeviceList: Error decoding devices", err)
utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DL004")
return
}
}
// Respond with the list of devices
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": devices,
})
}

View file

@ -0,0 +1,99 @@
package constellation
import (
"net/http"
"encoding/json"
"io/ioutil"
"os"
"github.com/azukaar/cosmos-server/src/utils"
)
func API_GetConfig(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "GET") {
// read utils.CONFIGFOLDER + "nebula.yml"
config, err := ioutil.ReadFile(utils.CONFIGFOLDER + "nebula.yml")
if err != nil {
utils.Error("SettingGet: error while reading nebula.yml", err)
utils.HTTPError(w, "Error while reading nebula.yml", http.StatusInternalServerError, "HTTP002")
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": string(config),
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func API_Restart(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "GET") {
RestartNebula()
utils.Log("Constellation: nebula restarted")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func API_Reset(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "GET") {
ResetNebula()
utils.Log("Constellation: nebula reset")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func API_GetLogs(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "GET") {
logs, err := os.ReadFile(utils.CONFIGFOLDER+"nebula.log")
if err != nil {
utils.Error("Error reading file:", err)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": string(logs),
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -0,0 +1,47 @@
package constellation
import (
"net/http"
"encoding/json"
"io/ioutil"
"github.com/azukaar/cosmos-server/src/utils"
)
func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "POST") {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
utils.Error("API_Restart: Invalid User Request", err)
utils.HTTPError(w, "API_Restart Error",
http.StatusInternalServerError, "AR001")
return
}
config := utils.ReadConfigFromFile()
config.ConstellationConfig.Enabled = true
config.ConstellationConfig.SlaveMode = true
config.ConstellationConfig.DNSDisabled = true
// ConstellationHostname =
// output utils.CONFIGFOLDER + "nebula.yml"
err = ioutil.WriteFile(utils.CONFIGFOLDER + "nebula.yml", body, 0644)
utils.SetBaseMainConfig(config)
RestartNebula()
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -0,0 +1,62 @@
package constellation
import (
"github.com/azukaar/cosmos-server/src/utils"
"os"
"time"
)
func Init() {
var err error
// if date is > 1st of January 2024
timeNow := time.Now()
if timeNow.Year() > 2024 || (timeNow.Year() == 2024 && timeNow.Month() > 1) {
utils.Error("Constellation: this preview version has expired, please update to use the lastest version of Constellation.", nil)
// disable constellation
configFile := utils.ReadConfigFromFile()
configFile.ConstellationConfig.Enabled = false
utils.SetBaseMainConfig(configFile)
return
}
// if Constellation is enabled
if utils.GetMainConfig().ConstellationConfig.Enabled {
if !utils.GetMainConfig().ConstellationConfig.SlaveMode {
InitConfig()
utils.Log("Initializing Constellation module...")
// check if ca.crt exists
if _, err = os.Stat(utils.CONFIGFOLDER + "ca.crt"); os.IsNotExist(err) {
utils.Log("Constellation: ca.crt not found, generating...")
// generate ca.crt
generateNebulaCACert("Cosmos - " + utils.GetMainConfig().ConstellationConfig.ConstellationHostname)
}
// check if cosmos.crt exists
if _, err := os.Stat(utils.CONFIGFOLDER + "cosmos.crt"); os.IsNotExist(err) {
utils.Log("Constellation: cosmos.crt not found, generating...")
// generate cosmos.crt
generateNebulaCert("cosmos", "192.168.201.1/24", "", true)
}
// export nebula.yml
utils.Log("Constellation: exporting nebula.yml...")
err := ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
if err != nil {
utils.Error("Constellation: error while exporting nebula.yml", err)
}
}
// start nebula
utils.Log("Constellation: starting nebula...")
err = startNebulaInBackground()
if err != nil {
utils.Error("Constellation: error while starting nebula", err)
}
utils.Log("Constellation module initialized")
}
}

542
src/constellation/nebula.go Normal file
View file

@ -0,0 +1,542 @@
package constellation
import (
"github.com/azukaar/cosmos-server/src/utils"
"os/exec"
"os"
"fmt"
"errors"
"runtime"
"sync"
"gopkg.in/yaml.v2"
"strings"
"io/ioutil"
"strconv"
"encoding/json"
"io"
"github.com/natefinch/lumberjack"
)
var logBuffer *lumberjack.Logger
var (
process *exec.Cmd
processMux sync.Mutex
)
func binaryToRun() string {
if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" {
return "./nebula-arm"
}
return "./nebula"
}
func startNebulaInBackground() error {
processMux.Lock()
defer processMux.Unlock()
if process != nil {
return errors.New("nebula is already running")
}
logBuffer = &lumberjack.Logger{
Filename: utils.CONFIGFOLDER+"nebula.log",
MaxSize: 1, // megabytes
MaxBackups: 1,
MaxAge: 15, //days
Compress: false,
}
process = exec.Command(binaryToRun(), "-config", utils.CONFIGFOLDER+"nebula.yml")
// Set up multi-writer for stderr
process.Stderr = io.MultiWriter(logBuffer, os.Stderr)
if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
// Set up multi-writer for stdout if in debug mode
process.Stdout = io.MultiWriter(logBuffer, os.Stdout)
} else {
process.Stdout = io.MultiWriter(logBuffer)
}
// Start the process in the background
if err := process.Start(); err != nil {
return err
}
utils.Log(fmt.Sprintf("%s started with PID %d\n", binaryToRun(), process.Process.Pid))
return nil
}
func stop() error {
processMux.Lock()
defer processMux.Unlock()
if process == nil {
return nil
}
if err := process.Process.Kill(); err != nil {
return err
}
process = nil
utils.Log("Stopped nebula.")
return nil
}
func RestartNebula() {
stop()
Init()
}
func ResetNebula() error {
stop()
utils.Log("Resetting nebula...")
os.RemoveAll(utils.CONFIGFOLDER + "nebula.yml")
os.RemoveAll(utils.CONFIGFOLDER + "ca.crt")
os.RemoveAll(utils.CONFIGFOLDER + "ca.key")
os.RemoveAll(utils.CONFIGFOLDER + "cosmos.crt")
os.RemoveAll(utils.CONFIGFOLDER + "cosmos.key")
// remove everything in db
c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
if err != nil {
return err
}
_, err = c.DeleteMany(nil, map[string]interface{}{})
if err != nil {
return err
}
config := utils.ReadConfigFromFile()
config.ConstellationConfig.Enabled = false
config.ConstellationConfig.SlaveMode = false
config.ConstellationConfig.DNSDisabled = false
utils.SetBaseMainConfig(config)
Init()
return nil
}
func GetAllLightHouses() ([]utils.ConstellationDevice, error) {
c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
if err != nil {
return []utils.ConstellationDevice{}, err
}
var devices []utils.ConstellationDevice
cursor, err := c.Find(nil, map[string]interface{}{
"IsLighthouse": true,
"Blocked": false,
})
cursor.All(nil, &devices)
if err != nil {
return []utils.ConstellationDevice{}, err
}
return devices, nil
}
func GetBlockedDevices() ([]utils.ConstellationDevice, error) {
c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
if err != nil {
return []utils.ConstellationDevice{}, err
}
var devices []utils.ConstellationDevice
cursor, err := c.Find(nil, map[string]interface{}{
"Blocked": true,
})
cursor.All(nil, &devices)
if err != nil {
return []utils.ConstellationDevice{}, err
}
return devices, nil
}
func cleanIp(ip string) string {
return strings.Split(ip, "/")[0]
}
func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath string) error {
// Combine defaultConfig and overwriteConfig
finalConfig := NebulaDefaultConfig
if !overwriteConfig.PrivateNode {
finalConfig.StaticHostMap = map[string][]string{
"192.168.201.1": []string{
utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
},
}
} else {
finalConfig.StaticHostMap = map[string][]string{}
}
// for each lighthouse
lh, err := GetAllLightHouses()
if err != nil {
return err
}
for _, l := range lh {
finalConfig.StaticHostMap[cleanIp(l.IP)] = []string{
l.PublicHostname + ":" + l.Port,
}
}
// add blocked devices
blockedDevices, err := GetBlockedDevices()
if err != nil {
return err
}
for _, d := range blockedDevices {
finalConfig.PKI.Blocklist = append(finalConfig.PKI.Blocklist, d.Fingerprint)
}
finalConfig.Lighthouse.AMLighthouse = !overwriteConfig.PrivateNode
// add other lighthouses
finalConfig.Lighthouse.Hosts = []string{}
for _, l := range lh {
finalConfig.Lighthouse.Hosts = append(finalConfig.Lighthouse.Hosts, cleanIp(l.IP))
}
finalConfig.Relay.AMRelay = overwriteConfig.NebulaConfig.Relay.AMRelay
finalConfig.Relay.Relays = []string{}
for _, l := range lh {
if l.IsRelay {
finalConfig.Relay.Relays = append(finalConfig.Relay.Relays, cleanIp(l.IP))
}
}
// Marshal the combined config to YAML
yamlData, err := yaml.Marshal(finalConfig)
if err != nil {
return err
}
// delete nebula.yml if exists
if _, err := os.Stat(outputPath); err == nil {
os.Remove(outputPath)
}
// Write YAML data to the specified file
yamlFile, err := os.Create(outputPath)
if err != nil {
return err
}
defer yamlFile.Close()
_, err = yamlFile.Write(yamlData)
if err != nil {
return err
}
return nil
}
func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, device utils.ConstellationDevice) (string, error) {
utils.Log("Exporting YAML config for " + name + " with file " + configPath)
// Read the YAML config file
yamlData, err := ioutil.ReadFile(configPath)
if err != nil {
return "", err
}
// Unmarshal the YAML data into a map interface
var configMap map[string]interface{}
err = yaml.Unmarshal(yamlData, &configMap)
if err != nil {
return "", err
}
lh, err := GetAllLightHouses()
if err != nil {
return "", err
}
if staticHostMap, ok := configMap["static_host_map"].(map[interface{}]interface{}); ok {
if !utils.GetMainConfig().ConstellationConfig.PrivateNode {
staticHostMap["192.168.201.1"] = []string{
utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
}
}
for _, l := range lh {
staticHostMap[cleanIp(l.IP)] = []string{
l.PublicHostname + ":" + l.Port,
}
}
} else {
return "", errors.New("static_host_map not found in nebula.yml")
}
// set lightHouse
if lighthouseMap, ok := configMap["lighthouse"].(map[interface{}]interface{}); ok {
lighthouseMap["am_lighthouse"] = device.IsLighthouse
lighthouseMap["hosts"] = []string{}
if !utils.GetMainConfig().ConstellationConfig.PrivateNode {
lighthouseMap["hosts"] = append(lighthouseMap["hosts"].([]string), "192.168.201.1")
}
for _, l := range lh {
if cleanIp(l.IP) != cleanIp(device.IP) {
lighthouseMap["hosts"] = append(lighthouseMap["hosts"].([]string), cleanIp(l.IP))
}
}
} else {
return "", errors.New("lighthouse not found in nebula.yml")
}
if pkiMap, ok := configMap["pki"].(map[interface{}]interface{}); ok {
pkiMap["ca"] = capki
pkiMap["cert"] = cert
pkiMap["key"] = key
} else {
return "", errors.New("pki not found in nebula.yml")
}
if relayMap, ok := configMap["relay"].(map[interface{}]interface{}); ok {
relayMap["am_relay"] = device.IsRelay && device.IsLighthouse
relayMap["relays"] = []string{}
if utils.GetMainConfig().ConstellationConfig.NebulaConfig.Relay.AMRelay {
relayMap["relays"] = append(relayMap["relays"].([]string), "192.168.201.1")
}
for _, l := range lh {
if l.IsRelay && l.IsLighthouse && cleanIp(l.IP) != cleanIp(device.IP) {
relayMap["relays"] = append(relayMap["relays"].([]string), cleanIp(l.IP))
}
}
} else {
return "", errors.New("relay not found in nebula.yml")
}
if listen, ok := configMap["listen"].(map[interface{}]interface{}); ok {
if device.Port != "" {
listen["port"] = device.Port
} else {
listen["port"] = "4242"
}
} else {
return "", errors.New("listen not found in nebula.yml")
}
configMap["constellation_device_name"] = name
configMap["constellation_local_dns_overwrite"] = true
configMap["constellation_local_dns_overwrite_address"] = "192.168.201.1"
configMap["constellation_public_hostname"] = device.PublicHostname
configMap["constellation_api_key"] = APIKey
// export configMap as YML
yamlData, err = yaml.Marshal(configMap)
if err != nil {
return "", err
}
return string(yamlData), nil
}
func getCApki() (string, error) {
// read config/ca.crt
caCrt, err := ioutil.ReadFile(utils.CONFIGFOLDER + "ca.crt")
if err != nil {
return "", err
}
return string(caCrt), nil
}
func killAllNebulaInstances() error {
processMux.Lock()
defer processMux.Unlock()
cmd := exec.Command("ps", "-e", "-o", "pid,command")
output, err := cmd.CombinedOutput()
if err != nil {
return err
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, binaryToRun()) {
fields := strings.Fields(line)
if len(fields) > 1 {
pid := fields[0]
pidInt, _ := strconv.Atoi(pid)
process, err := os.FindProcess(pidInt)
if err != nil {
return err
}
err = process.Kill()
if err != nil {
return err
}
utils.Log(fmt.Sprintf("Killed Nebula instance with PID %s\n", pid))
}
}
}
return nil
}
func GetCertFingerprint(certPath string) (string, error) {
// nebula-cert print -json
var cmd *exec.Cmd
cmd = exec.Command(binaryToRun() + "-cert",
"print",
"-json",
"-path", certPath,
)
// capture and parse output
output, err := cmd.CombinedOutput()
if err != nil {
utils.Error("Error while printing cert", err)
}
var certInfo map[string]interface{}
err = json.Unmarshal(output, &certInfo)
if err != nil {
utils.Error("Error while unmarshalling cert information", err)
return "", err
}
// Extract fingerprint, replace "fingerprint" with the actual key where the fingerprint is stored in the JSON output
fingerprint, ok := certInfo["fingerprint"].(string)
if !ok {
utils.Error("Fingerprint not found or not a string", nil)
return "", errors.New("fingerprint not found or not a string")
}
return fingerprint, nil
}
func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, string, error) {
// Run the nebula-cert command
var cmd *exec.Cmd
if(PK == "") {
cmd = exec.Command(binaryToRun() + "-cert",
"sign",
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
"-ca-key", utils.CONFIGFOLDER + "ca.key",
"-name", name,
"-ip", ip,
)
} else {
// write PK to temp.cert
err := ioutil.WriteFile("./temp.key", []byte(PK), 0644)
if err != nil {
return "", "", "", fmt.Errorf("failed to write temp.key: %s", err)
}
cmd = exec.Command(binaryToRun() + "-cert",
"sign",
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
"-ca-key", utils.CONFIGFOLDER + "ca.key",
"-name", name,
"-ip", ip,
"-in-pub", "./temp.key",
)
// delete temp.key
defer os.Remove("./temp.key")
}
utils.Debug(cmd.String())
cmd.Stderr = os.Stderr
if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
cmd.Stdout = os.Stdout
} else {
cmd.Stdout = nil
}
cmd.Run()
if cmd.ProcessState.ExitCode() != 0 {
return "", "", "", fmt.Errorf("nebula-cert exited with an error, check the Cosmos logs")
}
// Read the generated certificate and key files
certPath := fmt.Sprintf("./%s.crt", name)
keyPath := fmt.Sprintf("./%s.key", name)
utils.Debug("Reading certificate from " + certPath)
utils.Debug("Reading key from " + keyPath)
fingerprint, err := GetCertFingerprint(certPath)
if err != nil {
return "", "", "", fmt.Errorf("failed to get certificate fingerprint: %s", err)
}
certContent, errCert := ioutil.ReadFile(certPath)
if errCert != nil {
return "", "", "", fmt.Errorf("failed to read certificate file: %s", errCert)
}
keyContent, errKey := ioutil.ReadFile(keyPath)
if errKey != nil {
return "", "", "", fmt.Errorf("failed to read key file: %s", errKey)
}
if saveToFile {
cmd = exec.Command("mv", certPath, utils.CONFIGFOLDER + name + ".crt")
utils.Debug(cmd.String())
cmd.Run()
cmd = exec.Command("mv", keyPath, utils.CONFIGFOLDER + name + ".key")
utils.Debug(cmd.String())
cmd.Run()
} else {
// Delete the generated certificate and key files
if err := os.Remove(certPath); err != nil {
return "", "", "", fmt.Errorf("failed to delete certificate file: %s", err)
}
if err := os.Remove(keyPath); err != nil {
return "", "", "", fmt.Errorf("failed to delete key file: %s", err)
}
}
return string(certContent), string(keyContent), fingerprint, nil
}
func generateNebulaCACert(name string) (error) {
// Run the nebula-cert command to generate CA certificate and key
cmd := exec.Command(binaryToRun() + "-cert", "ca", "-name", "\""+name+"\"")
utils.Debug(cmd.String())
cmd.Stderr = os.Stderr
if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
cmd.Stdout = os.Stdout
} else {
cmd.Stdout = nil
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("nebula-cert error: %s", err)
}
// copy to /config/ca.*
cmd = exec.Command("mv", "./ca.crt", utils.CONFIGFOLDER + "ca.crt")
cmd.Run()
cmd = exec.Command("mv", "./ca.key", utils.CONFIGFOLDER + "ca.key")
cmd.Run()
return nil
}

View file

@ -0,0 +1,113 @@
package constellation
import (
"github.com/azukaar/cosmos-server/src/utils"
)
var NebulaDefaultConfig utils.NebulaConfig
func InitConfig() {
NebulaDefaultConfig = utils.NebulaConfig {
PKI: struct {
CA string `yaml:"ca"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Blocklist []string `yaml:"blocklist"`
}{
CA: utils.CONFIGFOLDER + "ca.crt",
Cert: utils.CONFIGFOLDER + "cosmos.crt",
Key: utils.CONFIGFOLDER + "cosmos.key",
Blocklist: []string{},
},
StaticHostMap: map[string][]string{
},
Lighthouse: struct {
AMLighthouse bool `yaml:"am_lighthouse"`
Interval int `yaml:"interval"`
Hosts []string `yaml:"hosts"`
}{
AMLighthouse: true,
Interval: 60,
Hosts: []string{},
},
Listen: struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
}{
Host: "0.0.0.0",
Port: 4242,
},
Punchy: struct {
Punch bool `yaml:"punch"`
Respond bool `yaml:"respond"`
}{
Punch: true,
Respond: true,
},
Relay: struct {
AMRelay bool `yaml:"am_relay"`
UseRelays bool `yaml:"use_relays"`
Relays []string `yaml:"relays"`
}{
AMRelay: true,
UseRelays: true,
Relays: []string{},
},
TUN: struct {
Disabled bool `yaml:"disabled"`
Dev string `yaml:"dev"`
DropLocalBroadcast bool `yaml:"drop_local_broadcast"`
DropMulticast bool `yaml:"drop_multicast"`
TxQueue int `yaml:"tx_queue"`
MTU int `yaml:"mtu"`
Routes []string `yaml:"routes"`
UnsafeRoutes []string `yaml:"unsafe_routes"`
}{
Disabled: false,
Dev: "nebula1",
DropLocalBroadcast: false,
DropMulticast: false,
TxQueue: 500,
MTU: 1300,
Routes: nil,
UnsafeRoutes: nil,
},
Logging: struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
}{
Level: "info",
Format: "text",
},
Firewall: struct {
OutboundAction string `yaml:"outbound_action"`
InboundAction string `yaml:"inbound_action"`
Conntrack utils.NebulaConntrackConfig `yaml:"conntrack"`
Outbound []utils.NebulaFirewallRule `yaml:"outbound"`
Inbound []utils.NebulaFirewallRule `yaml:"inbound"`
}{
OutboundAction: "drop",
InboundAction: "drop",
Conntrack: utils.NebulaConntrackConfig{
TCPTimeout: "12m",
UDPTimeout: "3m",
DefaultTimeout: "10m",
},
Outbound: []utils.NebulaFirewallRule {
utils.NebulaFirewallRule {
Host: "any",
Port: "any",
Proto: "any",
},
},
Inbound: []utils.NebulaFirewallRule {
utils.NebulaFirewallRule {
Host: "any",
Port: "any",
Proto: "any",
},
},
},
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/azukaar/cosmos-server/src/docker"
"github.com/azukaar/cosmos-server/src/authorizationserver"
"github.com/azukaar/cosmos-server/src/market"
"github.com/azukaar/cosmos-server/src/constellation"
"github.com/gorilla/mux"
"strconv"
"time"
@ -331,6 +332,13 @@ func InitServer() *mux.Router {
srapi.HandleFunc("/api/background", UploadBackground)
srapi.HandleFunc("/api/background/{ext}", GetBackground)
srapi.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
srapi.HandleFunc("/api/constellation/restart", constellation.API_Restart)
srapi.HandleFunc("/api/constellation/reset", constellation.API_Reset)
srapi.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)
srapi.HandleFunc("/api/constellation/config", constellation.API_GetConfig)
srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
srapi.Use(utils.EnsureHostname)

View file

@ -9,6 +9,7 @@ import (
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/authorizationserver"
"github.com/azukaar/cosmos-server/src/market"
"github.com/azukaar/cosmos-server/src/constellation"
)
func main() {
@ -44,5 +45,9 @@ func main() {
authorizationserver.Init()
constellation.InitDNS()
constellation.Init()
StartServer()
}

View file

@ -12,7 +12,7 @@ type marketGetResult struct {
}
func MarketGet(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
if utils.LoggedInOnly(w, req) != nil {
return
}
@ -22,7 +22,6 @@ func MarketGet(w http.ResponseWriter, req *http.Request) {
return
}
// return the first 10 results of each market
marketGetResult := marketGetResult{
All: make(map[string]interface{}),
Showcase: []appDefinition{},

View file

@ -46,7 +46,7 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardHeader bool, DisableHeaderHardening bool, CORSOrigin string) (*httputil.ReverseProxy, error) {
func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardHeader bool, DisableHeaderHardening bool, CORSOrigin string, route utils.ProxyRouteConfig) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
@ -76,16 +76,40 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardH
req.Header.Set("X-Forwarded-Ssl", "on")
}
if CORSOrigin != "" {
req.Header.Set("X-Forwarded-Host", url.Host)
req.Header.Del("X-Origin-Host")
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-For")
req.Header.Del("X-Real-Ip")
// hide hostname (dangerous)
// req.Header.Del("Host")
hostname := utils.GetMainConfig().HTTPConfig.Hostname
if route.Host != "" && route.UseHost {
hostname = route.Host
}
if route.UsePathPrefix {
hostname = hostname + route.PathPrefix
}
hostDest := hostname
if route.OverwriteHostHeader != "" {
hostDest = route.OverwriteHostHeader
}
// hide hostname (dangerous)
// req.Header.Set("Host", url.Host)
// req.Host = url.Host
req.Header.Set("Host", hostDest)
req.Host = hostDest
if VerboseForwardHeader {
req.Header.Set("X-Origin-Host", url.Host)
req.Header.Set("Host", url.Host)
req.Header.Set("X-Origin-Host", hostDest)
req.Header.Set("X-Forwarded-Host", hostDest)
req.Header.Set("X-Forwarded-For", utils.GetClientIP(req))
req.Header.Set("X-Real-IP", utils.GetClientIP(req))
}
}
}
if AcceptInsecureHTTPSTarget && url.Scheme == "https" {
@ -100,8 +124,6 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardH
if CORSOrigin != "" {
resp.Header.Del("Access-Control-Allow-Origin")
resp.Header.Del("Access-Control-Allow-Methods")
resp.Header.Del("Access-Control-Allow-Headers")
resp.Header.Del("Access-Control-Allow-Credentials")
}
@ -126,7 +148,7 @@ func RouteTo(route utils.ProxyRouteConfig) http.Handler {
routeType := route.Mode
if(routeType == "SERVAPP" || routeType == "PROXY") {
proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget, route.VerboseForwardHeader, route.DisableHeaderHardening, route.CORSOrigin)
proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget, route.VerboseForwardHeader, route.DisableHeaderHardening, route.CORSOrigin, route)
if err != nil {
utils.Error("Create Route", err)
}

View file

@ -30,12 +30,12 @@ func tokenMiddleware(enabled bool, adminOnly bool) func(next http.Handler) http.
r.Header.Set("x-cosmos-mfa", strconv.Itoa((int)(u.MFAState)))
ogcookies := r.Header.Get("Cookie")
cookieRemoveRegex := regexp.MustCompile(`jwttoken=[^;]*;`)
cookieRemoveRegex := regexp.MustCompile(`\s?jwttoken=[^;]*;?\s?`)
cookies := cookieRemoveRegex.ReplaceAllString(ogcookies, "")
r.Header.Set("Cookie", cookies)
// Replace the token with a application speicfic one
r.Header.Set("x-cosmos-token", "1234567890")
//r.Header.Set("x-cosmos-token", "1234567890")
if enabled && adminOnly {
if errT := utils.AdminOnlyWithRedirect(w, r); errT != nil {
@ -85,6 +85,8 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination htt
}
}
destination = utils.Restrictions(route.RestrictToConstellation, route.WhitelistInboundIPs)(destination)
destination = SmartShieldMiddleware(route.Name, route.SmartShield)(destination)
originCORS := route.CORSOrigin

View file

@ -2,12 +2,9 @@ package user
import (
"net/http"
// "io"
// "os"
"encoding/json"
"go.mongodb.org/mongo-driver/mongo"
"time"
// "golang.org/x/crypto/bcrypt"
"github.com/azukaar/cosmos-server/src/utils"
)

View file

@ -5,6 +5,7 @@ import (
"github.com/azukaar/cosmos-server/src/utils"
"github.com/golang-jwt/jwt"
"errors"
"strconv"
"strings"
"time"
"encoding/json"
@ -189,13 +190,25 @@ func logOutUser(w http.ResponseWriter, req *http.Request) {
HttpOnly: true,
}
clientCookie := http.Cookie{
Name: "client-infos",
Value: "{}",
Expires: time.Now().Add(-time.Hour * 24 * 365),
Path: "/",
Secure: utils.IsHTTPS,
HttpOnly: false,
}
if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" {
cookie.Domain = ""
clientCookie.Domain = ""
} else {
cookie.Domain = "." + reqHostNoPort
clientCookie.Domain = "." + reqHostNoPort
}
http.SetCookie(w, &cookie)
http.SetCookie(w, &clientCookie)
// TODO: logout every other device if asked by increasing passwordcycle
}
@ -254,13 +267,24 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
HttpOnly: true,
}
clientCookie := http.Cookie{
Name: "client-infos",
Value: user.Nickname + "," + strconv.Itoa(int(user.Role)),
Expires: expiration,
Path: "/",
Secure: utils.IsHTTPS,
HttpOnly: false,
}
utils.Log("UserLogin: Setting cookie for " + reqHostNoPort)
if reqHostNoPort == "localhost" || reqHostNoPort == "0.0.0.0" {
cookie.Domain = ""
clientCookie.Domain = ""
} else {
if utils.IsValidHostname(reqHostNoPort) {
cookie.Domain = "." + reqHostNoPort
clientCookie.Domain = "." + reqHostNoPort
} else {
utils.Error("UserLogin: Invalid hostname", nil)
utils.HTTPError(w, "User Logging Error", http.StatusInternalServerError, "UL001")
@ -269,4 +293,5 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
}
http.SetCookie(w, &cookie)
http.SetCookie(w, &clientCookie)
}

View file

@ -181,20 +181,20 @@ func DoLetsEncrypt() (string, string) {
}
err = client.Challenge.SetDNS01Provider(provider)
}
} else {
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", config.HTTPConfig.HTTPPort))
if err != nil {
Error("LETSENCRYPT_HTTP01", err)
LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
return "", ""
}
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", config.HTTPConfig.HTTPPort))
if err != nil {
Error("LETSENCRYPT_HTTP01", err)
LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
return "", ""
}
err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", config.HTTPConfig.HTTPSPort))
if err != nil {
Error("LETSENCRYPT_TLS01", err)
LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
return "", ""
err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", config.HTTPConfig.HTTPSPort))
if err != nil {
Error("LETSENCRYPT_TLS01", err)
LetsEncryptErrors = append(LetsEncryptErrors, err.Error())
return "", ""
}
}
// New users will need to register

View file

@ -82,8 +82,6 @@ func CORSHeader(origin string) func(next http.Handler) http.Handler {
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
@ -260,4 +258,60 @@ func IsValidHostname(hostname string) bool {
}
return false
}
func IPInRange(ipStr, cidrStr string) (bool, error) {
_, cidrNet, err := net.ParseCIDR(cidrStr)
if err != nil {
return false, fmt.Errorf("parse CIDR range: %w", err)
}
ip := net.ParseIP(ipStr)
if ip == nil {
return false, fmt.Errorf("parse IP: invalid IP address")
}
return cidrNet.Contains(ip), nil
}
func Restrictions(RestrictToConstellation bool, WhitelistInboundIPs []string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
isUsingWhiteList := len(WhitelistInboundIPs) > 0
isInWhitelist := false
isInConstellation := strings.HasPrefix(ip, "192.168.201.") || strings.HasPrefix(ip, "192.168.202.")
for _, ipRange := range WhitelistInboundIPs {
if strings.Contains(ipRange, "/") {
if ok, _ := IPInRange(ip, ipRange); ok {
isInWhitelist = true
}
} else {
if ip == ipRange {
isInWhitelist = true
}
}
}
isInConstellationPassing := !RestrictToConstellation || isInConstellation
isWhitelistPassing := !isUsingWhiteList || isInWhitelist
// check if the request is coming from the constellation IP range 192.168.201.0/24
if (!isInConstellationPassing && !isWhitelistPassing) {
Log("Request from " + ip + " is blocked because of restrictions isInConstellationPassing: " + fmt.Sprintf("%v", isInConstellationPassing) + " and isWhitelistPassing: " + fmt.Sprintf("%v", isWhitelistPassing))
http.Error(w, "Access denied", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}

View file

@ -90,6 +90,7 @@ type Config struct {
MarketConfig MarketConfig
HomepageConfig HomepageConfig
ThemeConfig ThemeConfig
ConstellationConfig ConstellationConfig
}
type HomepageConfig struct {
@ -180,6 +181,9 @@ type ProxyRouteConfig struct {
DisableHeaderHardening bool
VerboseForwardHeader bool
AddionalFilters []AddionalFiltersConfig
RestrictToConstellation bool
OverwriteHostHeader string
WhitelistInboundIPs []string
}
type EmailConfig struct {
@ -205,4 +209,115 @@ type MarketConfig struct {
type MarketSource struct {
Name string
Url string
}
}
type ConstellationConfig struct {
Enabled bool
SlaveMode bool
PrivateNode bool
DNSDisabled bool
DNSPort string
DNSFallback string
DNSBlockBlacklist bool
DNSAdditionalBlocklists []string
CustomDNSEntries []ConstellationDNSEntry
NebulaConfig NebulaConfig
ConstellationHostname string
}
type ConstellationDNSEntry struct {
Type string
Key string
Value string
}
type ConstellationDevice struct {
Nickname string `json:"nickname"`
DeviceName string `json:"deviceName"`
PublicKey string `json:"publicKey"`
IP string `json:"ip"`
IsLighthouse bool `json:"isLighthouse"`
IsRelay bool `json:"isRelay"`
PublicHostname string `json:"publicHostname"`
Port string `json:"port"`
Blocked bool `json:"blocked"`
Fingerprint string `json:"fingerprint"`
APIKey string `json:"-"`
}
type NebulaFirewallRule struct {
Port string `yaml:"port"`
Proto string `yaml:"proto"`
Host string `yaml:"host"`
Groups []string `yaml:"groups,omitempty"omitempty"`
}
type NebulaConntrackConfig struct {
TCPTimeout string `yaml:"tcp_timeout"`
UDPTimeout string `yaml:"udp_timeout"`
DefaultTimeout string `yaml:"default_timeout"`
}
type NebulaConfig struct {
PKI struct {
CA string `yaml:"ca"`
Cert string `yaml:"cert"`
Key string `yaml:"key"`
Blocklist []string `yaml:"blocklist"`
} `yaml:"pki"`
StaticHostMap map[string][]string `yaml:"static_host_map"`
Lighthouse struct {
AMLighthouse bool `yaml:"am_lighthouse"`
Interval int `yaml:"interval"`
Hosts []string `yaml:"hosts"`
} `yaml:"lighthouse"`
Listen struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"listen"`
Punchy struct {
Punch bool `yaml:"punch"`
Respond bool `yaml:"respond"`
} `yaml:"punchy"`
Relay struct {
AMRelay bool `yaml:"am_relay"`
UseRelays bool `yaml:"use_relays"`
Relays []string `yaml:"relays"`
} `yaml:"relay"`
TUN struct {
Disabled bool `yaml:"disabled"`
Dev string `yaml:"dev"`
DropLocalBroadcast bool `yaml:"drop_local_broadcast"`
DropMulticast bool `yaml:"drop_multicast"`
TxQueue int `yaml:"tx_queue"`
MTU int `yaml:"mtu"`
Routes []string `yaml:"routes"`
UnsafeRoutes []string `yaml:"unsafe_routes"`
} `yaml:"tun"`
Logging struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
} `yaml:"logging"`
Firewall struct {
OutboundAction string `yaml:"outbound_action"`
InboundAction string `yaml:"inbound_action"`
Conntrack NebulaConntrackConfig `yaml:"conntrack"`
Outbound []NebulaFirewallRule `yaml:"outbound"`
Inbound []NebulaFirewallRule `yaml:"inbound"`
} `yaml:"firewall"`
}
type Device struct {
DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"`
PublicKey string `json:"publicKey",omitempty`
PrivateKey string `json:"privateKey",omitempty`
IP string `json:"ip",validate:"required,ipv4"`
}

View file

@ -37,6 +37,8 @@ var ReBootstrapContainer func(string) error
var LetsEncryptErrors = []string{}
var CONFIGFOLDER = "/config/"
var DefaultConfig = Config{
LoggingLevel: "INFO",
NewInstall: true,
@ -60,6 +62,17 @@ var DefaultConfig = Config{
Sources: []MarketSource{
},
},
ConstellationConfig: ConstellationConfig{
Enabled: false,
DNSDisabled: false,
DNSFallback: "8.8.8.8:53",
DNSAdditionalBlocklists: []string{
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts",
},
},
}
func FileExists(path string) bool {
@ -193,10 +206,23 @@ func LoadBaseMainConfig(config Config) {
if os.Getenv("COSMOS_SERVER_COUNTRY") != "" {
MainConfig.ServerCountry = os.Getenv("COSMOS_SERVER_COUNTRY")
}
if os.Getenv("COSMOS_CONFIG_FOLDER") != "" {
Log("Overwriting config folder with " + os.Getenv("COSMOS_CONFIG_FOLDER"))
CONFIGFOLDER = os.Getenv("COSMOS_CONFIG_FOLDER")
}
if MainConfig.DockerConfig.DefaultDataPath == "" {
MainConfig.DockerConfig.DefaultDataPath = "/usr"
}
if MainConfig.ConstellationConfig.ConstellationHostname == "" {
// if hostname is a domain add vpn. suffix otherwise use hostname
if IsDomain(MainConfig.HTTPConfig.Hostname) {
MainConfig.ConstellationConfig.ConstellationHostname = "vpn." + MainConfig.HTTPConfig.Hostname
} else {
MainConfig.ConstellationConfig.ConstellationHostname = MainConfig.HTTPConfig.Hostname
}
}
}
func GetMainConfig() Config {
@ -219,7 +245,7 @@ func GetConfigFileName() string {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
configFile = "/config/cosmos.config.json"
configFile = CONFIGFOLDER + "cosmos.config.json"
}
return configFile
@ -539,10 +565,33 @@ func GetNetworkUsage() NetworkStatus {
return NetworkStatus{}
}
func DownloadFile(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func GetClientIP(req *http.Request) string {
/*ip := req.Header.Get("X-Forwarded-For")
if ip == "" {
ip = req.RemoteAddr
}*/
return req.RemoteAddr
}
func IsDomain(domain string) bool {
// contains . and at least a letter and no special characters invalid in a domain
if strings.Contains(domain, ".") && strings.ContainsAny(domain, "abcdefghijklmnopqrstuvwxyz") && !strings.ContainsAny(domain, " !@#$%^&*()+=[]{}\\|;:'\",/<>?") {
return true
}
return false
}