Update to 0.7.0-unstable5

This commit is contained in:
Yann Stepienik 2023-06-13 22:30:11 +01:00
parent 8d113b5337
commit aa9a72b787
25 changed files with 1636 additions and 108 deletions

View File

@ -1,3 +1,10 @@
## Version 0.7.0
- Add Cosmos Market
- Fix issue with docker compose timeout healthcheck as string, and inverted ports
## Version 0.6.0
// todo
## Version 0.6.0
- OpenID support!
- Add hostname check when adding new routes to Cosmos

View File

@ -73,4 +73,18 @@
.loading-image {
background: url('/assets/images/icons/cosmos_gray.png') no-repeat center center;
}
.raw-table table {
width: 100%;
border-collapse: collapse;
}
.raw-table table th {
text-align: left;
}
.raw-table table th, .raw-table table td {
padding: 5px;
border: 1px solid #ccc;
}

View File

@ -183,7 +183,7 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
: <CosmosInputText
name="Target"
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
placeholder={formik.values.Mode == "PROXY" ? "http://localhost:8080" : "/path/to/my/app"}
formik={formik}
/>
}

View File

@ -30,6 +30,7 @@ import RestartModal from './restart';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts';
import CountrySelect, { countries } from '../../../components/countrySelect';
import { DnsChallengeComp } from '../../../utils/dns-challenge-comp';
const ConfigManagement = () => {
@ -70,6 +71,7 @@ const ConfigManagement = () => {
UseWildcardCertificate: config.HTTPConfig.UseWildcardCertificate,
HTTPSCertificateMode: config.HTTPConfig.HTTPSCertificateMode,
DNSChallengeProvider: config.HTTPConfig.DNSChallengeProvider,
DNSChallengeConfig: config.HTTPConfig.DNSChallengeConfig,
Email_Enabled: config.EmailConfig.Enabled,
Email_Host: config.EmailConfig.Host,
@ -103,6 +105,7 @@ const ConfigManagement = () => {
UseWildcardCertificate: values.UseWildcardCertificate,
HTTPSCertificateMode: values.HTTPSCertificateMode,
DNSChallengeProvider: values.DNSChallengeProvider,
DNSChallengeConfig: values.DNSChallengeConfig,
},
EmailConfig: {
...config.EmailConfig,
@ -371,6 +374,18 @@ const ConfigManagement = () => {
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
/>
</Stack>
</Grid>
<CosmosSelect
name="HTTPSCertificateMode"
label="HTTPS Certificates"
@ -400,26 +415,15 @@ const ConfigManagement = () => {
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
<DnsChallengeComp
label="Pick a DNS provider (if you are using a DNS Challenge, otherwise leave empty)"
name="DNSChallengeProvider"
label="DNS provider (if you are using a DNS Challenge)"
configName="DNSChallengeConfig"
formik={formik}
/>
)
}
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<h4>Authentication Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>

View File

@ -33,7 +33,7 @@ import defaultport from '../../servapps/defaultport.json';
import * as API from '../../../api';
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange}) {
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
const [open, setOpen] = React.useState(false);
const [containers, setContainers] = React.useState([]);
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
@ -42,8 +42,6 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
const [portsOptions, setPortsOptions] = React.useState(null);
const loading = options === null;
const name = "Target"
const label = "Container Name"
let targetResult = {
container: 'null',
port: "",
@ -51,6 +49,7 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
}
let preview = formik.values[name];
console.log(name)
if(preview) {
let protocols = preview.split("://")

View File

@ -151,8 +151,8 @@ const MarketPage = () => {
height: '100%',
}}></Link>
<Stack direction="row" spacing={2} style={{ height: '100%', overflow: 'hidden' }} justifyContent="flex-end">
<Stack direction="column" spacing={3} style={{ height: '100%' }} sx={{
<Stack direction="row" spacing={2} style={{ height: '100%'}} justifyContent="flex-end">
<Stack direction="column" spacing={3} style={{ height: '100%', overflow: "auto"}} sx={{
backgroundColor: isDark ? '#1A2027' : '#fff',
padding: '80px 80px',
width: '100%',
@ -193,9 +193,7 @@ const MarketPage = () => {
<div><strong>compose:</strong> <LinkMUI href={openedApp.compose}>{openedApp.compose}</LinkMUI></div>
</div>
<div dangerouslySetInnerHTML={{ __html: openedApp.longDescription }} style={{
overflow: 'hidden',
}}></div>
<div dangerouslySetInnerHTML={{ __html: openedApp.longDescription }}></div>
<div>
<DockerComposeImport installerInit defaultName={openedApp.name} dockerComposeInit={openedApp.compose} />

View File

@ -20,6 +20,7 @@ import AnimateButton from '../../components/@extended/AnimateButton';
import { Box } from '@mui/system';
import { pull } from 'lodash';
import { isDomain } from '../../utils/indexs';
import { DnsChallengeComp } from '../../utils/dns-challenge-comp';
// ================================|| LOGIN ||================================ //
const debounce = (func, wait) => {
@ -272,6 +273,7 @@ const NewInstall = () => {
HTTPSCertificateMode: "LETSENCRYPT",
UseWildcardCertificate: false,
DNSChallengeProvider: '',
DNSChallengeConfig: {},
}}
validationSchema={Yup.object().shape({
SSLEmail: Yup.string().when('HTTPSCertificateMode', {
@ -305,6 +307,7 @@ const NewInstall = () => {
TLSCert: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSCert : '',
Hostname: values.Hostname,
DNSChallengeProvider: values.DNSChallengeProvider,
DNSChallengeConfig: values.DNSChallengeConfig,
});
if(res.status == "OK") {
setStatus({ success: true });
@ -350,10 +353,10 @@ const NewInstall = () => {
Cosmos after this installer. See doc here: <a target="_blank" href="https://go-acme.github.io/lego/dns/">https://go-acme.github.io/lego/dns/</a>
</Alert>
)}
<CosmosInputText
label={"DNS Provider (only set if you want to use the DNS challenge)"}
<DnsChallengeComp
label="Pick a DNS provider (if you are using a DNS Challenge, otherwise leave empty)"
name="DNSChallengeProvider"
placeholder={"provider"}
configName="DNSChallengeConfig"
formik={formik}
/>
</>

View File

@ -26,10 +26,14 @@ import ResponsiveButton from '../../../components/responseiveButton';
import UploadButtons from '../../../components/fileUpload';
import NewDockerService from './newService';
import yaml from 'js-yaml';
import { CosmosCollapse, CosmosFormDivider, CosmosInputText } from '../../config/users/formShortcuts';
import { CosmosCollapse, CosmosFormDivider, CosmosInputPassword, CosmosInputText } from '../../config/users/formShortcuts';
import VolumeContainerSetup from './volumes';
import DockerContainerSetup from './setup';
import whiskers from 'whiskers';
import {version} from '../../../../../package.json';
import cmp from 'semver-compare';
import { HostnameChecker } from '../../../utils/routes';
import { CosmosContainerPicker } from '../../config/users/containerPicker';
function checkIsOnline() {
API.isOnline().then((res) => {
@ -67,8 +71,8 @@ const preStyle = {
marginRight: '0',
}
const isNewerVersion = (v1, v2) => {
return false;
const isNewerVersion = (minver) => {
return cmp(version, minver) === -1;
}
const getHostnameFromName = (name) => {
@ -224,6 +228,11 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
if (!doc.services[key].container_name) {
doc.services[key].container_name = key;
}
// convert healthcheck
if (doc.services[key].healthcheck && typeof doc.services[key].healthcheck.timeout === 'string') {
doc.services[key].healthcheck.timeout = parseInt(doc.services[key].healthcheck.timeout);
}
});
}
@ -293,7 +302,8 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
try {
if (installer) {
const rendered = whiskers.render(dockerCompose, {
console.log(hostnames)
const rendered = whiskers.render(dockerCompose.replace(/{StaticServiceName}/ig, serviceName), {
ServiceName: serviceName,
Hostnames: hostnames,
Context: context,
@ -333,13 +343,34 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
let newVolumes = [];
// SET DEFAULT CONTEXT
let newContext = {};
if (jsoned['cosmos-installer'] && jsoned['cosmos-installer'].form) {
jsoned['cosmos-installer'].form.forEach((field) => {
[field.name, field['name-container']].forEach((fieldName) => {
if(typeof context[fieldName] === "undefined" && typeof field.initialValue !== "undefined") {
newContext[fieldName] = field.initialValue;
} else if (typeof context[fieldName] !== "undefined") {
newContext[fieldName] = context[fieldName];
}
});
});
}
if(JSON.stringify(Object.keys(newContext)) !== JSON.stringify(Object.keys(context)))
setContext({...newContext});
Object.keys(jsoned.services).forEach((key) => {
// APPLY OVERRIDE
if (overrides[key]) {
// prevent customizing static volumes
if (jsoned.services[key].volumes && jsoned['cosmos-installer'] && jsoned['cosmos-installer']['frozen-volumes']) {
jsoned['cosmos-installer']['frozen-volumes'].forEach((volume) => {
delete overrides[key].volumes;
jsoned['cosmos-installer']['frozen-volumes'].forEach((volumeName) => {
const keyVolume = overrides[key].volumes.findIndex((v) => {
console.log(v)
return v.source === volumeName;
});
delete overrides[key].volumes[keyVolume];
});
}
@ -363,7 +394,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
// CREATE NEW VOLUMES
if (jsoned.services[key].volumes) {
jsoned.services[key].volumes.forEach((volume) => {
if (typeof volume === 'object' && !volume.existing) {
if (typeof volume === 'object' && !volume.source.startsWith('/') && !volume.existing) {
newVolumes.push(volume);
} else if (typeof volume === 'object' && volume.existing) {
delete volume.existing;
@ -396,6 +427,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
setYmlError(null);
setOverrides({});
setHostnames({});
setContext({});
setDockerCompose('');
setInstaller(installerInit);
setServiceName(defaultName || 'default-name');
@ -452,15 +484,61 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
<TextField label="Service Name" value={serviceName} onChange={(e) => setServiceName(e.target.value)} />
{service['cosmos-installer'] && service['cosmos-installer'].form.map((formElement) => {
return formElement.type === 'checkbox' ? <FormControlLabel
control={<Checkbox checked={context[formElement.name] || formElement.initialValue} onChange={(e) => {
return formElement.type === 'checkbox' ?
<FormControlLabel
control={<Checkbox checked={context[formElement.name]} onChange={(e) => {
setContext({ ...context, [formElement.name]: e.target.checked });
}
} />}
label={formElement.label}
/> : <TextField
/> : (formElement.type === 'password' || formElement.type === 'email') ?
<TextField
label={formElement.label}
value={context[formElement.name] || formElement.initialValue}
value={context[formElement.name]}
type={formElement.type}
onChange={(e) => {
setContext({ ...context, [formElement.name]: e.target.value });
}
} />
: formElement.type === 'hostname' ?
<>
<TextField
label={formElement.label}
value={context[formElement.name]}
onChange={(e) => {
setContext({ ...context, [formElement.name]: e.target.value });
}
} />
<HostnameChecker hostname={context[formElement.name]} />
</>
: formElement.type === 'container' || formElement.type === 'container-full' ?
<CosmosContainerPicker
name={formElement.name}
formik={{
values: {
[formElement.name] : context[formElement.name]
},
errors: {},
setFieldValue: (name, value) => {
setContext({ ...context, [formElement.name]: value });
},
}}
nameOnly={formElement.type === 'container'}
label={formElement.label}
onTargetChange={(_, name) => {
console.log(formElement['name-container'], name)
console.log(context)
setContext({ ...context, [formElement['name-container']]: name });
}}
/>
: formElement.type === 'error' || formElement.type === 'info' || formElement.type === 'warning' ?
<Alert severity={formElement.type}>
{formElement.label}
</Alert>
: <TextField
label={formElement.label}
value={context[formElement.name]}
onChange={(e) => {
setContext({ ...context, [formElement.name]: e.target.value });
}
@ -479,6 +557,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
hostnames[serviceIndex][hostname.name].host = e.target.value;
setHostnames({...hostnames});
}} />
<HostnameChecker hostname={hostname.host} />
</>
})
})}
@ -518,7 +597,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
containerInfo={{
HostConfig: {
Binds: [],
Mounts: Object.keys(value.volumes).map(k => {
Mounts: value.volumes && Object.keys(value.volumes).map(k => {
return {
Type: value.volumes[k].type || (k.startsWith('/') ? 'bind' : 'volume'),
Source: value.volumes[k].source || "",
@ -560,14 +639,23 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
</Stack>}
</DialogContentText>
</DialogContent>
{!isLoading && <DialogActions>
{(installerInit && service.minVersion && isNewerVersion(service.minVersion)) ?
<Alert severity="error" icon={<WarningOutlined />}>
This service requires a newer version of Cosmos. Please update Cosmos to install this service.
</Alert>
:
(!isLoading && <DialogActions>
<Button onClick={() => {
setOpenModal(false);
setStep(0);
setDockerCompose('');
setYmlError('');
setInstaller(false);
}}>Cancel</Button>
setServiceName('');
setContext({});
setHostnames({});
setOverrides({});
}}>Close</Button>
<Button disabled={!dockerCompose || ymlError} onClick={() => {
if (step === 0) {
setStep(1);
@ -578,24 +666,20 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
{step === 0 && 'Next'}
{step === 1 && 'Back'}
</Button>
</DialogActions>}
</DialogActions>)}
</Dialog>
{(installerInit && service.minVersion && isNewerVersion(service.minVersion)) ?
<Alert severity="error" icon={<WarningOutlined />}>
This service requires a newer version of Cosmos. Please update Cosmos to install this service.
</Alert>
: <ResponsiveButton
color="primary"
onClick={() => {
openModalFunc();
}}
variant={(installerInit ? "contained" : "outlined")}
startIcon={(installerInit ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
>
{installerInit ? 'Install' : 'Import Compose File'}
</ResponsiveButton>
}
<ResponsiveButton
color="primary"
onClick={() => {
openModalFunc();
}}
variant={(installerInit ? "contained" : "outlined")}
startIcon={(installerInit ? <ArrowDownOutlined /> : <ArrowUpOutlined />)}
>
{installerInit ? 'Install' : 'Import Compose File'}
</ResponsiveButton>
</>;
};

View File

@ -146,18 +146,6 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
<div>
{formik.values.ports.map((port, idx) => (
<Grid container key={idx}>
<Grid item xs={4} style={{ padding }}>
<TextField
label="Container Port"
fullWidth
value={port.port}
onChange={(e) => {
const newports = [...formik.values.ports];
newports[idx].port = e.target.value;
formik.setFieldValue('ports', newports);
}}
/>
</Grid>
<Grid item xs={4} style={{ padding }}>
<TextField
fullWidth
@ -170,6 +158,18 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
}}
/>
</Grid>
<Grid item xs={4} style={{ padding }}>
<TextField
label="Container Port"
fullWidth
value={port.port}
onChange={(e) => {
const newports = [...formik.values.ports];
newports[idx].port = e.target.value;
formik.setFieldValue('ports', newports);
}}
/>
</Grid>
<Grid item xs={3} style={{ padding }}>
<TextField
className="px-2 my-2"

View File

@ -60,6 +60,8 @@ const NewDockerService = ({service, refresh}) => {
const [openModal, setOpenModal] = React.useState(false);
const preRef = React.useRef(null);
const installer = {...service['cosmos-installer']};
service = {...service};
delete service['cosmos-installer'];
React.useEffect(() => {
@ -99,6 +101,9 @@ const NewDockerService = ({service, refresh}) => {
>Create</LoadingButton>}
{isDone && <Stack spacing={1}>
<Alert severity="success">Service Created!</Alert>
{installer && installer['post-install'] && installer['post-install'].map(m =>{
return <Alert severity={m.type}>{m.label}</Alert>
})}
{needsRestart && <Alert severity="warning">Cosmos needs to be restarted to apply changes to the URLs</Alert>}
{needsRestart &&
<Button

View File

@ -0,0 +1,84 @@
import dnsList from './dns-list.json';
import dnsConfig from './dns-config.json';
import * as React from 'react';
import {
Checkbox,
Divider,
FormControlLabel,
Grid,
InputLabel,
OutlinedInput,
Stack,
Typography,
FormHelperText,
TextField,
MenuItem,
AccordionSummary,
AccordionDetails,
Accordion,
Chip,
Box,
FormControl,
IconButton,
InputAdornment,
Alert,
} from '@mui/material';
import { Field } from 'formik';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { CosmosCollapse, CosmosSelect } from '../pages/config/users/formShortcuts';
export const DnsChallengeComp = ({ name, configName, style, multiline, type, placeholder, onChange, label, formik }) => {
const [dnsSetConfig, setDnsSetConfig] = useState({});
return <><CosmosSelect
name={name}
label={label}
formik={formik}
onChange={(e) => {
onChange && onChange(e);
}}
options={dnsList.map((dns) => ([dns,dns]))}
/>
<Grid item xs={12}>
<Stack spacing={2}>
{formik.values[name] && dnsConfig[formik.values[name]] &&<>
{dnsConfig[formik.values[name]].vars.length > 0 && <CosmosCollapse title="DNS Challenge setup">
<Stack spacing={2}>
<Alert severity="info">
Please be careful you are filling the correct values. Check the doc if unsure. Leave blank unused variables. <br />
Doc link: <a href={dnsConfig[formik.values[name]].url} target="_blank">{dnsConfig[formik.values[name]].url}</a>
</Alert>
<div className="raw-table">
<div dangerouslySetInnerHTML={{__html: dnsConfig[formik.values[name]].docs}}></div>
</div>
{dnsConfig[formik.values[name]].vars.map((dnsVar) => <div>
{dnsVar}:
<OutlinedInput
type={type ? type : 'text'}
value={formik.values[configName][dnsVar] || ''}
onChange={(...ar) => {
const newConfig = {
...formik.values[configName],
[dnsVar]: ar[0].target.value
};
formik.setFieldValue(configName, newConfig);
}}
placeholder={"leave blank if unused"}
fullWidth
/>
</div>)}
</Stack>
</CosmosCollapse>}
</>}
</Stack>
</Grid>
</>;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,117 @@
[
"acme-dns",
"alidns",
"allinkl",
"arvancloud",
"azure",
"auroradns",
"autodns",
"bindman",
"bluecat",
"brandit",
"bunny",
"checkdomain",
"civo",
"clouddns",
"cloudflare",
"cloudns",
"cloudxns",
"conoha",
"constellix",
"derak",
"desec",
"designate",
"digitalocean",
"dnshomede",
"dnsimple",
"dnsmadeeasy",
"dnspod",
"dode",
"domeneshop",
"dreamhost",
"duckdns",
"dyn",
"dynu",
"easydns",
"edgedns",
"epik",
"exoscale",
"freemyip",
"gandi",
"gandiv5",
"gcloud",
"gcore",
"glesys",
"godaddy",
"googledomains",
"hetzner",
"hostingde",
"hosttech",
"httpreq",
"hurricane",
"ibmcloud",
"iij",
"iijdpf",
"infoblox",
"infomaniak",
"internetbs",
"inwx",
"ionos",
"iwantmyname",
"joker",
"liara",
"lightsail",
"linode",
"liquidweb",
"luadns",
"loopia",
"mydnsjp",
"mythicbeasts",
"namecheap",
"namedotcom",
"namesilo",
"nearlyfreespeech",
"netcup",
"netlify",
"nicmanager",
"nifcloud",
"njalla",
"nodion",
"ns1",
"oraclecloud",
"otc",
"ovh",
"pdns",
"plesk",
"porkbun",
"rackspace",
"regru",
"rfc2136",
"rimuhosting",
"route53",
"safedns",
"sakuracloud",
"scaleway",
"selectel",
"servercow",
"simply",
"sonic",
"stackpath",
"tencentcloud",
"transip",
"ultradns",
"variomedia",
"vegadns",
"vercel",
"versio",
"vinyldns",
"vkcloud",
"vscale",
"vultr",
"websupport",
"wedos",
"yandex",
"yandexcloud",
"zoneee",
"zonomi"
]

View File

@ -27,23 +27,11 @@ export function isDomain(hostname) {
return true;
}
export function debounce(func, wait, immediate) {
var timeout;
return () => {
var context = this, args = arguments;
var later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
export const debounce = (func, wait) => {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
timeout = setTimeout(() => func.apply(context, args), wait);
};
};

View File

@ -3,9 +3,14 @@ import demoicons from './icons.demo.json';
import logogray from '../assets/images/icons/cosmos_gray.png';
import * as Yup from 'yup';
import { Alert, Grid } from '@mui/material';
import { debounce, isDomain } from './indexs';
import * as API from '../api';
import { useEffect, useState } from 'react';
export const sanitizeRoute = (_route) => {
let route = {..._route};
let route = { ..._route };
if (!route.UseHost) {
route.Host = "";
@ -13,14 +18,14 @@ export const sanitizeRoute = (_route) => {
if (!route.UsePathPrefix) {
route.PathPrefix = "";
}
route.Name = route.Name.trim();
if(!route.SmartShield) {
if (!route.SmartShield) {
route.SmartShield = {};
}
if(typeof route._SmartShield_Enabled !== "undefined") {
if (typeof route._SmartShield_Enabled !== "undefined") {
route.SmartShield.Enabled = route._SmartShield_Enabled;
delete route._SmartShield_Enabled;
}
@ -46,13 +51,13 @@ export const getFullOrigin = (route) => {
const isDemo = import.meta.env.MODE === 'demo';
export const getFaviconURL = (route) => {
if(isDemo) {
if (isDemo) {
if (route.Mode == "STATIC")
return Folder;
return demoicons[route.Name] || logogray;
}
if(!route) {
if (!route) {
return logogray;
}
@ -60,7 +65,7 @@ export const getFaviconURL = (route) => {
return '/cosmos/api/favicon?q=' + encodeURIComponent(url)
}
if(route.Mode == "SERVAPP" || route.Mode == "PROXY") {
if (route.Mode == "SERVAPP" || route.Mode == "PROXY") {
return addRemote(route.Target)
} else if (route.Mode == "STATIC") {
return Folder;
@ -75,6 +80,9 @@ export const ValidateRouteSchema = Yup.object().shape({
Target: Yup.string().required('Target is required').when('Mode', {
is: 'SERVAPP',
then: Yup.string().matches(/:[0-9]+$/, 'Invalid Target, must have a port'),
}).when('Mode', {
is: 'PROXY',
then: Yup.string().matches(/^(https?:\/\/)/, 'Invalid Target, must start with http:// or https://'),
}),
Host: Yup.string().when('UseHost', {
@ -96,7 +104,7 @@ export const ValidateRouteSchema = Yup.object().shape({
})
export const ValidateRoute = (routeConfig, config) => {
let routeNames= config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name);
let routeNames = config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name);
try {
ValidateRouteSchema.validateSync(routeConfig);
@ -114,4 +122,36 @@ export const getContainersRoutes = (config, containerName) => {
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
return route.Mode == "SERVAPP" && reg.test(route.Target)
})) || [];
}
}
const checkHost = debounce((host, setHostError, setHostIp) => {
console.log(host)
if (isDomain(host)) {
API.getDNS(host).then((data) => {
setHostError(null)
setHostIp(data.data)
}).catch((err) => {
setHostError(err.message)
setHostIp(null)
});
} else {
setHostError(null);
setHostIp(null);
}
}, 500)
export const HostnameChecker = ({hostname}) => {
const [hostError, setHostError] = useState(null);
const [hostIp, setHostIp] = useState(null);
useEffect(() => {
if (hostname) {
checkHost(hostname, setHostError, setHostIp);
}
}, [hostname]);
return <>{hostError && <Alert color='error'>{hostError}</Alert>}
{hostIp && <Alert color='info'>This hostname is pointing to <strong>{hostIp}</strong>, make sure it is your server IP!</Alert>}
</>
};

32
gen-doc-dns.js Normal file
View File

@ -0,0 +1,32 @@
const dnsList = require('./client/src/utils/dns-list.json');
console.log(dnsList);
// for each make a request to https://go-acme.github.io/lego/dns/{dns}/#credentials
let finalList = {};
let i = 0;
(async () => {
for(const dns of dnsList) {
console.log(`Fetching ${dns} infos`)
let result = (await (await fetch(`https://go-acme.github.io/lego/dns/${dns}/#credentials`)).text());
result = result.split(`<h2 id="credentials">Credentials</h2>`)[1];
result = result.split(`</table>`)[0] + `</table>`;
let vars = result.match(/<code>(.*?)<\/code>/g);
vars = vars.map(v => v.replace(/<\/?code>/g, ''));
finalList[dns] = {
name: dns,
url: `https://go-acme.github.io/lego/dns/${dns}/#credentials`,
docs: result,
vars: vars
}
console.log(`${i++}/${dnsList.length} done`)
}
// save to file
const fs = require('fs');
fs.writeFileSync('./client/src/utils/dns-config.json', JSON.stringify(finalList, null, 2));
})();

10
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "cosmos-server",
"version": "0.7.0-unstable2",
"version": "0.7.0-unstable4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cosmos-server",
"version": "0.7.0-unstable2",
"version": "0.7.0-unstable4",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0",
@ -48,6 +48,7 @@
"react-syntax-highlighter": "^15.5.0",
"react-window": "^1.8.7",
"redux": "^4.2.0",
"semver-compare": "^1.0.0",
"simplebar": "^5.3.8",
"simplebar-react": "^2.4.1",
"typescript": "4.8.3",
@ -9160,6 +9161,11 @@
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.7.0-unstable4",
"version": "0.7.0-unstable5",
"description": "",
"main": "test-server.js",
"bugs": {
@ -48,6 +48,7 @@
"react-syntax-highlighter": "^15.5.0",
"react-window": "^1.8.7",
"redux": "^4.2.0",
"semver-compare": "^1.0.0",
"simplebar": "^5.3.8",
"simplebar-react": "^2.4.1",
"typescript": "4.8.3",

View File

@ -5,6 +5,8 @@
<!-- sponsors -->
<h3 align="center">Thanks to the sponsors:</h3></br>
<p align="center"><a href="https://github.com/zarevskaya"><img src="https://avatars.githubusercontent.com/zarevskaya" style="border-radius:48px" width="48" height="48" alt="zarev" title="zarev" /></a>
<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/JustDalek"><img src="https://avatars.githubusercontent.com/JustDalek" style="border-radius:48px" width="48" height="48" alt="JustDalek" title="JustDalek" /></a>
</p><!-- /sponsors -->
---

View File

@ -37,6 +37,7 @@ type ContainerCreateRequestContainer struct {
Volumes []mount.Mount `json:"volumes"`
Networks map[string]ContainerCreateRequestServiceNetwork `json:"networks"`
Routes []utils.ProxyRouteConfig `json:"routes"`
Links []string `json:"links,omitempty"`
RestartPolicy string `json:"restart,omitempty"`
Devices []string `json:"devices"`
@ -49,6 +50,8 @@ type ContainerCreateRequestContainer struct {
Entrypoint string `json:"entrypoint,omitempty"`
WorkingDir string `json:"working_dir,omitempty"`
User string `json:"user,omitempty"`
UID int `json:"uid,omitempty"`
GID int `json:"gid,omitempty"`
Hostname string `json:"hostname,omitempty"`
Domainname string `json:"domainname,omitempty"`
MacAddress string `json:"mac_address,omitempty"`
@ -66,7 +69,6 @@ type ContainerCreateRequestContainer struct {
DNS []string `json:"dns,omitempty"`
DNSSearch []string `json:"dns_search,omitempty"`
ExtraHosts []string `json:"extra_hosts,omitempty"`
Links []string `json:"links,omitempty"`
SecurityOpt []string `json:"security_opt,omitempty"`
StorageOpt map[string]string `json:"storage_opt,omitempty"`
Sysctls map[string]string `json:"sysctls,omitempty"`
@ -412,8 +414,8 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
PortBindings := nat.PortMap{}
for _, port := range container.Ports {
portContainer := strings.Split(port, ":")[0]
portHost := strings.Split(port, ":")[1]
portHost := strings.Split(port, ":")[0]
portContainer := strings.Split(port, ":")[1]
containerConfig.ExposedPorts[nat.Port(portContainer)] = struct{}{}
PortBindings[nat.Port(portContainer)] = []nat.PortBinding{
@ -445,20 +447,26 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
utils.Log(fmt.Sprintf("Not found. Creating directory %s for bind mount", newSource))
OnLog(fmt.Sprintf("Not found. Creating directory %s for bind mount\n", newSource))
err := os.MkdirAll(newSource, 0755)
err := os.MkdirAll(newSource, 0750)
if err != nil {
utils.Error("CreateService: Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory", err)
OnLog(fmt.Sprintf("[ERROR] Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory: %s\n", err.Error()))
Rollback(rollbackActions, OnLog)
return err
}
if container.User != "" {
if container.UID != 0 {
// Change the ownership of the directory to the container.UID
err = os.Chown(newSource, container.UID, container.GID)
if err != nil {
utils.Error("CreateService: Unable to change ownership of directory", err)
OnLog(fmt.Sprintf("[ERROR] Unable to change ownership of directory: " + err.Error()))
}
} else if container.User != "" {
// Change the ownership of the directory to the container.User
userInfo, err := user.Lookup(container.User)
if err != nil {
utils.Error("CreateService: Unable to lookup user", err)
OnLog(fmt.Sprintf("[ERROR] Unable to lookup user " + container.User + "." +err.Error()))
OnLog(fmt.Sprintf("[ERROR] Unable to lookup user " + container.User + ". " +err.Error()))
} else {
uid, _ := strconv.Atoi(userInfo.Uid)
gid, _ := strconv.Atoi(userInfo.Gid)
@ -484,7 +492,6 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
DNS: container.DNS,
DNSSearch: container.DNSSearch,
ExtraHosts: container.ExtraHosts,
Links: container.Links,
SecurityOpt: container.SecurityOpt,
StorageOpt: container.StorageOpt,
Sysctls: container.Sysctls,
@ -566,6 +573,26 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
}
}
// Create the networks for links
for _, targetContainer := range container.Links {
if strings.Contains(targetContainer, ":") {
err = errors.New("Link network cannot contain ':' please use container name only")
utils.Error("CreateService: Rolling back changes because of -- Link network", err)
OnLog(fmt.Sprintf("[ERROR] Rolling back changes because of -- Link network creation error: "+err.Error()))
Rollback(rollbackActions, OnLog)
return err
}
err = CreateLinkNetwork(container.Name, targetContainer)
if err != nil {
utils.Error("CreateService: Rolling back changes because of -- Link network", err)
OnLog(fmt.Sprintf("[ERROR] Rolling back changes because of -- Link network creation error: "+err.Error()))
Rollback(rollbackActions, OnLog)
return err
}
}
// Write a response to the client
utils.Log(fmt.Sprintf("Container %s created", container.Name))
OnLog(fmt.Sprintf("Container %s created", container.Name))

View File

@ -246,6 +246,41 @@ func _debounceNetworkCleanUp() func(string) {
}
}
func CreateLinkNetwork(containerName string, container2Name string) error {
// create network
networkName := "cosmos-link-" + containerName + "-" + container2Name + "-" + utils.GenerateRandomString(2)
_, err := DockerClient.NetworkCreate(DockerContext, networkName, types.NetworkCreate{
CheckDuplicate: true,
Labels: map[string]string{
"cosmos-link": "true",
"cosmos-link-name": networkName,
"cosmos-link-container1": containerName,
"cosmos-link-container2": container2Name,
},
})
if err != nil {
return err
}
// connect containers to network
err = ConnectToNetworkSync(networkName, containerName)
if err != nil {
return err
}
err = ConnectToNetworkSync(networkName, container2Name)
if err != nil {
// disconnect first container
DockerClient.NetworkDisconnect(DockerContext, networkName, containerName, true)
// destroy network
DockerClient.NetworkRemove(DockerContext, networkName)
return err
}
return nil
}
var DebouncedNetworkCleanUp = _debounceNetworkCleanUp()
func NetworkCleanUp() {

View File

@ -57,6 +57,13 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
var certReloader *simplecert.CertReloader
var errSimCert error
if(config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
if(config.HTTPConfig.DNSChallengeProvider != "") {
newEnv := config.HTTPConfig.DNSChallengeConfig
for key, value := range newEnv {
os.Setenv(key, value)
}
}
certReloader, errSimCert = simplecert.Init(cfg, nil)
if errSimCert != nil {
// Temporary before we have a better way to handle this

View File

@ -34,6 +34,7 @@ type NewInstallJSON struct {
SSLEmail string `json:"sslEmail",validate:"omitempty,email"`
UseWildcardCertificate bool `json:"useWildcardCertificate",validate:"omitempty"`
DNSChallengeProvider string `json:"dnsChallengeProvider",validate:"omitempty"`
DNSChallengeConfig map[string]string
}
type AdminJSON struct {
@ -110,6 +111,7 @@ func NewInstallRoute(w http.ResponseWriter, req *http.Request) {
newConfig.HTTPConfig.SSLEmail = request.SSLEmail
newConfig.HTTPConfig.UseWildcardCertificate = request.UseWildcardCertificate
newConfig.HTTPConfig.DNSChallengeProvider = request.DNSChallengeProvider
newConfig.HTTPConfig.DNSChallengeConfig = request.DNSChallengeConfig
newConfig.HTTPConfig.TLSCert = request.TLSCert
newConfig.HTTPConfig.TLSKey = request.TLSKey

View File

@ -32,7 +32,7 @@ func UserLogin(w http.ResponseWriter, req *http.Request) {
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
utils.HTTPError(w, "Database Error", http.StatusInternalServerError, "DB001")
return
}

View File

@ -105,6 +105,7 @@ type HTTPConfig struct {
SSLEmail string `validate:"omitempty,email"`
UseWildcardCertificate bool
AcceptAllInsecureHostname bool
DNSChallengeConfig map[string]string
}
const (