[release] version 0.6.0-unstable
This commit is contained in:
parent
0cdb11ba82
commit
60bf7627bb
|
@ -1,3 +1,9 @@
|
||||||
|
## Version 0.6.0
|
||||||
|
- OpenID support!
|
||||||
|
- Add hostname check when adding new routes to Cosmos
|
||||||
|
- Add hostname check on new Install
|
||||||
|
- Fix missing save button for network mode
|
||||||
|
|
||||||
## Version 0.5.11
|
## Version 0.5.11
|
||||||
- Improve docker-compose import support for alternative syntaxes
|
- Improve docker-compose import support for alternative syntaxes
|
||||||
- Improve docker service creation when using force secure label (fixes few containers not liking restarting too fast when created)
|
- Improve docker service creation when using force secure label (fixes few containers not liking restarting too fast when created)
|
||||||
|
|
|
@ -101,6 +101,52 @@ let newInstall = (req, onProgress) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkHost = (host) => {
|
||||||
|
return fetch('/cosmos/api/dns-check?url=' + host, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(async (response) => {
|
||||||
|
let rep;
|
||||||
|
try {
|
||||||
|
rep = await response.json();
|
||||||
|
} catch {
|
||||||
|
throw new Error('Server error');
|
||||||
|
}
|
||||||
|
if (response.status == 200) {
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
const e = new Error(rep.message);
|
||||||
|
e.status = response.status;
|
||||||
|
e.message = rep.message;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDNS = (host) => {
|
||||||
|
return fetch('/cosmos/api/dns?url=' + host, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(async (response) => {
|
||||||
|
let rep;
|
||||||
|
try {
|
||||||
|
rep = await response.json();
|
||||||
|
} catch {
|
||||||
|
throw new Error('Server error');
|
||||||
|
}
|
||||||
|
if (response.status == 200) {
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
const e = new Error(rep.message);
|
||||||
|
e.status = response.status;
|
||||||
|
e.message = rep.message;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isDemo = import.meta.env.MODE === 'demo';
|
const isDemo = import.meta.env.MODE === 'demo';
|
||||||
|
|
||||||
let auth = _auth;
|
let auth = _auth;
|
||||||
|
@ -125,5 +171,7 @@ export {
|
||||||
docker,
|
docker,
|
||||||
getStatus,
|
getStatus,
|
||||||
newInstall,
|
newInstall,
|
||||||
isOnline
|
isOnline,
|
||||||
|
checkHost,
|
||||||
|
getDNS
|
||||||
};
|
};
|
|
@ -4,16 +4,19 @@ import { useEffect } from 'react';
|
||||||
|
|
||||||
const IsLoggedIn = () => useEffect(() => {
|
const IsLoggedIn = () => useEffect(() => {
|
||||||
console.log("CHECK LOGIN")
|
console.log("CHECK LOGIN")
|
||||||
|
const urlSearch = encodeURIComponent(window.location.search);
|
||||||
|
const redirectTo = (window.location.pathname + urlSearch);
|
||||||
|
|
||||||
API.auth.me().then((data) => {
|
API.auth.me().then((data) => {
|
||||||
if(data.status != 'OK') {
|
if(data.status != 'OK') {
|
||||||
if(data.status == 'NEW_INSTALL') {
|
if(data.status == 'NEW_INSTALL') {
|
||||||
window.location.href = '/ui/newInstall';
|
window.location.href = '/ui/newInstall';
|
||||||
} else if (data.status == 'error' && data.code == "HTTP004") {
|
} else if (data.status == 'error' && data.code == "HTTP004") {
|
||||||
window.location.href = '/ui/login';
|
window.location.href = '/ui/login?redirect=' + redirectTo;
|
||||||
} else if (data.status == 'error' && data.code == "HTTP006") {
|
} else if (data.status == 'error' && data.code == "HTTP006") {
|
||||||
window.location.href = '/ui/loginmfa';
|
window.location.href = '/ui/loginmfa?redirect=' + redirectTo;
|
||||||
} else if (data.status == 'error' && data.code == "HTTP007") {
|
} else if (data.status == 'error' && data.code == "HTTP007") {
|
||||||
window.location.href = '/ui/newmfa';
|
window.location.href = '/ui/newmfa?redirect=' + redirectTo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
106
client/src/pages/authentication/openid.jsx
Normal file
106
client/src/pages/authentication/openid.jsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { Checkbox, Grid, Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
// project import
|
||||||
|
import AuthLogin from './auth-forms/AuthLogin';
|
||||||
|
import AuthWrapper from './AuthWrapper';
|
||||||
|
import { getFaviconURL } from '../../utils/routes';
|
||||||
|
import IsLoggedIn from '../../isLoggedIn';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
|
import { Field, useFormik } from 'formik';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// ================================|| LOGIN ||================================ //
|
||||||
|
|
||||||
|
const OpenID = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const client_id = searchParams.get("client_id")
|
||||||
|
const redirect_uri = searchParams.get("redirect_uri")
|
||||||
|
const scope = searchParams.get("scope")
|
||||||
|
const entireSearch = searchParams.toString()
|
||||||
|
const [checkedScopes, setCheckedScopes] = useState(["openid"])
|
||||||
|
|
||||||
|
// get hostname from redirect_uri with port
|
||||||
|
const port = new URL(redirect_uri).port
|
||||||
|
const protocol = new URL(redirect_uri).protocol + "//"
|
||||||
|
const appHostname = protocol + (new URL(redirect_uri).hostname) + (port ? ":" + port : "")
|
||||||
|
const icon = getFaviconURL({
|
||||||
|
Mode: 'PROXY',
|
||||||
|
Target: appHostname
|
||||||
|
});
|
||||||
|
|
||||||
|
const selfport = new URL(window.location.href).port
|
||||||
|
const selfprotocol = new URL(window.location.href).protocol + "//"
|
||||||
|
const selfHostname = selfprotocol + (new URL(window.location.href).hostname) + (selfport ? ":" + selfport : "")
|
||||||
|
|
||||||
|
const onchange = (e, scope) => {
|
||||||
|
console.log(scope)
|
||||||
|
if (e.target.checked) {
|
||||||
|
setCheckedScopes([...checkedScopes,scope])
|
||||||
|
} else {
|
||||||
|
setCheckedScopes(checkedScopes.filter((scope) => scope != scope))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<AuthWrapper>
|
||||||
|
<IsLoggedIn />
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Typography variant="h3">Login with OpenID - {client_id}</Typography>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="baseline" spacing={2} style={{
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<img src={icon} alt={'icon'} width="64px" />
|
||||||
|
<div>
|
||||||
|
You are logging in to <b>{client_id}</b>. <br />
|
||||||
|
Check which permissions you are giving to this application. <br />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<link rel="openid2.provider openid.server" href={selfHostname + "/oauth2/auth"} />
|
||||||
|
<form action={"/oauth2/auth?" + entireSearch} method="post">
|
||||||
|
<input type="hidden" name="client_id" value={client_id} />
|
||||||
|
{scope.split(' ').map((scope) => {
|
||||||
|
return scope == "openid" ? <div>
|
||||||
|
<input type="checkbox" name="scopes" value={scope} checked hidden />
|
||||||
|
<Checkbox checked disabled />
|
||||||
|
account
|
||||||
|
</div>
|
||||||
|
: <div>
|
||||||
|
<input type="checkbox" name="scopes" hidden value={scope} checked={checkedScopes.includes(scope)} />
|
||||||
|
<Checkbox onChange={(e) => onchange(e, scope)} />
|
||||||
|
{scope}
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
marginTop: '15px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
opacity: '0.8',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}}>
|
||||||
|
You will be redirected to <b>{appHostname}</b> after login. <br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
disableElevation
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
OpenID Login
|
||||||
|
</LoadingButton>
|
||||||
|
</form>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AuthWrapper>)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OpenID;
|
|
@ -27,6 +27,8 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const routes = config.HTTPConfig.ProxyConfig.Routes || [];
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||||
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
|
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
|
||||||
|
@ -56,7 +58,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
routeNames={routes.map((r) => r.Name)}
|
||||||
setRouteConfig={(_newRoute) => {
|
setRouteConfig={(_newRoute) => {
|
||||||
setNewRoute(sanitizeRoute(_newRoute));
|
setNewRoute(sanitizeRoute(_newRoute));
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from
|
||||||
import { CosmosContainerPicker } from '../users/containerPicker';
|
import { CosmosContainerPicker } from '../users/containerPicker';
|
||||||
import { snackit } from '../../../api/wrap';
|
import { snackit } from '../../../api/wrap';
|
||||||
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
|
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
|
||||||
|
import { isDomain } from '../../../utils/indexs';
|
||||||
|
|
||||||
const Hide = ({ children, h }) => {
|
const Hide = ({ children, h }) => {
|
||||||
return h ? <div style={{ display: 'none' }}>
|
return h ? <div style={{ display: 'none' }}>
|
||||||
|
@ -31,8 +32,20 @@ const debounce = (func, wait) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkHost = debounce((host, setHostError) => {
|
||||||
|
if(isDomain(host)) {
|
||||||
|
API.checkHost(host).then((data) => {
|
||||||
|
setHostError(null)
|
||||||
|
}).catch((err) => {
|
||||||
|
setHostError(err.message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
|
const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
|
||||||
const [openModal, setOpenModal] = React.useState(false);
|
const [openModal, setOpenModal] = React.useState(false);
|
||||||
|
const [hostError, setHostError] = React.useState(null);
|
||||||
|
|
||||||
|
|
||||||
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||||
|
@ -186,13 +199,21 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
|
||||||
formik={formik}
|
formik={formik}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{formik.values.UseHost && <CosmosInputText
|
{formik.values.UseHost && (<><CosmosInputText
|
||||||
name="Host"
|
name="Host"
|
||||||
label="Host"
|
label="Host"
|
||||||
placeholder="Host"
|
placeholder="Host"
|
||||||
formik={formik}
|
formik={formik}
|
||||||
style={{ paddingLeft: '20px' }}
|
style={{ paddingLeft: '20px' }}
|
||||||
/>}
|
onChange={(e) => {
|
||||||
|
checkHost(e.target.value, setHostError)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{hostError && <Grid item xs={12}>
|
||||||
|
<Alert color='error'>{hostError}</Alert>
|
||||||
|
</Grid>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<CosmosCheckbox
|
<CosmosCheckbox
|
||||||
name="UsePathPrefix"
|
name="UsePathPrefix"
|
||||||
|
|
|
@ -38,7 +38,10 @@ export const CosmosInputText = ({ name, style, multiline, type, placeholder, onC
|
||||||
name={name}
|
name={name}
|
||||||
multiline={multiline}
|
multiline={multiline}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
onChange={formik.handleChange}
|
onChange={(...ar) => {
|
||||||
|
onChange && onChange(...ar);
|
||||||
|
return formik.handleChange(...ar);
|
||||||
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
fullWidth
|
fullWidth
|
||||||
error={Boolean(formik.touched[name] && formik.errors[name])}
|
error={Boolean(formik.touched[name] && formik.errors[name])}
|
||||||
|
|
|
@ -159,7 +159,7 @@ const HomePage = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Grid2 container spacing={2} style={{ zIndex: 2 }}>
|
<Grid2 container spacing={2} style={{ zIndex: 2 }}>
|
||||||
{config && serveApps && config.HTTPConfig.ProxyConfig.Routes.map((route) => {
|
{config && serveApps && routes.map((route) => {
|
||||||
let skip = false;
|
let skip = false;
|
||||||
if(route.Mode == "SERVAPP") {
|
if(route.Mode == "SERVAPP") {
|
||||||
const containerName = route.Target.split(':')[1].slice(2);
|
const containerName = route.Target.split(':')[1].slice(2);
|
||||||
|
@ -187,7 +187,7 @@ const HomePage = () => {
|
||||||
</Grid2>
|
</Grid2>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{config && config.HTTPConfig.ProxyConfig.Routes.length === 0 && (
|
{config && routes.length === 0 && (
|
||||||
<Grid2 item xs={12} sm={12} md={12} lg={12} xl={12}>
|
<Grid2 item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
<Box style={{ padding: 10, borderRadius: 5, ...appColor }}>
|
<Box style={{ padding: 10, borderRadius: 5, ...appColor }}>
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
|
|
|
@ -19,8 +19,30 @@ import { CosmosCheckbox, CosmosInputPassword, CosmosInputText, CosmosSelect } fr
|
||||||
import AnimateButton from '../../components/@extended/AnimateButton';
|
import AnimateButton from '../../components/@extended/AnimateButton';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { pull } from 'lodash';
|
import { pull } from 'lodash';
|
||||||
|
import { isDomain } from '../../utils/indexs';
|
||||||
// ================================|| LOGIN ||================================ //
|
// ================================|| LOGIN ||================================ //
|
||||||
|
|
||||||
|
const debounce = (func, wait) => {
|
||||||
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
const context = this;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkHost = debounce((host, setHostError, setHostIp) => {
|
||||||
|
if(isDomain(host)) {
|
||||||
|
API.getDNS(host).then((data) => {
|
||||||
|
setHostError(null)
|
||||||
|
setHostIp(data.data)
|
||||||
|
}).catch((err) => {
|
||||||
|
setHostError(err.message)
|
||||||
|
setHostIp(null)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
const NewInstall = () => {
|
const NewInstall = () => {
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
|
@ -29,6 +51,8 @@ const NewInstall = () => {
|
||||||
const [databaseEnable, setDatabaseEnable] = useState(true);
|
const [databaseEnable, setDatabaseEnable] = useState(true);
|
||||||
const [pullRequest, setPullRequest] = useState(null);
|
const [pullRequest, setPullRequest] = useState(null);
|
||||||
const [pullRequestOnSuccess, setPullRequestOnSuccess] = useState(null);
|
const [pullRequestOnSuccess, setPullRequestOnSuccess] = useState(null);
|
||||||
|
const [hostError, setHostError] = useState(null);
|
||||||
|
const [hostIp, setHostIp] = useState(null);
|
||||||
|
|
||||||
const refreshStatus = async () => {
|
const refreshStatus = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -355,7 +379,16 @@ const NewInstall = () => {
|
||||||
label="Hostname (Domain required for Let's Encrypt)"
|
label="Hostname (Domain required for Let's Encrypt)"
|
||||||
placeholder="yourdomain.com, your ip, or localhost"
|
placeholder="yourdomain.com, your ip, or localhost"
|
||||||
formik={formik}
|
formik={formik}
|
||||||
|
onChange={(e) => {
|
||||||
|
checkHost(e.target.value, setHostError, setHostIp);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{hostError && <Grid item xs={12}>
|
||||||
|
<Alert color='error'>{hostError}</Alert>
|
||||||
|
</Grid>}
|
||||||
|
{hostIp && <Grid item xs={12}>
|
||||||
|
<Alert color='info'>This hostname is pointing to <strong>{hostIp}</strong>, check that it is your server IP!</Alert>
|
||||||
|
</Grid>}
|
||||||
|
|
||||||
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && formik.values.UseWildcardCertificate && (!formik.values.DNSChallengeProvider || formik.values.DNSChallengeProvider == '') && (
|
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && formik.values.UseWildcardCertificate && (!formik.values.DNSChallengeProvider || formik.values.DNSChallengeProvider == '') && (
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
|
|
|
@ -95,6 +95,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const realvalues = {
|
const realvalues = {
|
||||||
portBindings: {},
|
portBindings: {},
|
||||||
|
networkMode: values.networkMode,
|
||||||
};
|
};
|
||||||
values.ports.forEach((port) => {
|
values.ports.forEach((port) => {
|
||||||
if (port.hostPort) {
|
if (port.hostPort) {
|
||||||
|
@ -122,7 +123,8 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
|
||||||
{(formik) => (
|
{(formik) => (
|
||||||
<form noValidate onSubmit={formik.handleSubmit}>
|
<form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<MainCard title={'Ports'}>
|
|
||||||
|
<MainCard title={'Network Settings'}>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
||||||
<Alert severity="warning" style={{ marginBottom: '0px' }}>
|
<Alert severity="warning" style={{ marginBottom: '0px' }}>
|
||||||
|
@ -134,6 +136,13 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
|
||||||
This container is forced to be secured. You cannot expose any ports to the internet directly, please create a URL in Cosmos instead. You also cannot connect it to the Bridge network.
|
This container is forced to be secured. You cannot expose any ports to the internet directly, please create a URL in Cosmos instead. You also cannot connect it to the Bridge network.
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
<CosmosInputText
|
||||||
|
label="Network Mode"
|
||||||
|
name="networkMode"
|
||||||
|
placeholder={'default'}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
<CosmosFormDivider title={'Expose Ports'} />
|
||||||
<div>
|
<div>
|
||||||
{formik.values.ports.map((port, idx) => (
|
{formik.values.ports.map((port, idx) => (
|
||||||
<Grid container key={idx}>
|
<Grid container key={idx}>
|
||||||
|
@ -239,13 +248,6 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
|
||||||
<MainCard title={'Networks'}>
|
<MainCard title={'Networks'}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
|
|
||||||
<CosmosInputText
|
|
||||||
label="Network Mode"
|
|
||||||
name="networkMode"
|
|
||||||
placeholder={'default'}
|
|
||||||
formik={formik}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{networks && <Stack spacing={2}>
|
{networks && <Stack spacing={2}>
|
||||||
{Object.keys(containerInfo.NetworkSettings.Networks).map((networkName) => {
|
{Object.keys(containerInfo.NetworkSettings.Networks).map((networkName) => {
|
||||||
const network = networks.find((n) => n.Name === networkName);
|
const network = networks.find((n) => n.Name === networkName);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import NewInstall from '../pages/newInstall/newInstall';
|
||||||
|
|
||||||
import {NewMFA, MFALogin} from '../pages/authentication/newMFA';
|
import {NewMFA, MFALogin} from '../pages/authentication/newMFA';
|
||||||
import ForgotPassword from '../pages/authentication/forgotPassword';
|
import ForgotPassword from '../pages/authentication/forgotPassword';
|
||||||
|
import OpenID from '../pages/authentication/openid';
|
||||||
|
|
||||||
// render - login
|
// render - login
|
||||||
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
|
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
|
||||||
|
@ -40,6 +41,10 @@ const LoginRoutes = {
|
||||||
path: '/ui/newmfa',
|
path: '/ui/newmfa',
|
||||||
element: <NewMFA />
|
element: <NewMFA />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/ui/openid',
|
||||||
|
element: <OpenID />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/ui/loginmfa',
|
path: '/ui/loginmfa',
|
||||||
element: <MFALogin />
|
element: <MFALogin />
|
||||||
|
|
|
@ -6,3 +6,23 @@ export const randomString = (length) => {
|
||||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDomain(hostname) {
|
||||||
|
// Regular expression to check if it's an IP address
|
||||||
|
const ipPattern = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||||
|
|
||||||
|
// Remove port if there is one
|
||||||
|
hostname = hostname.replace(/:\d+$/, '');
|
||||||
|
|
||||||
|
// Check if the hostname is an IP address
|
||||||
|
if (ipPattern.test(hostname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the hostname is "localhost"
|
||||||
|
if (hostname === 'localhost') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -110,7 +110,7 @@ export const ValidateRoute = (routeConfig, config) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getContainersRoutes = (config, containerName) => {
|
export const getContainersRoutes = (config, containerName) => {
|
||||||
return (config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
return (config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
||||||
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
|
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
|
||||||
return route.Mode == "SERVAPP" && reg.test(route.Target)
|
return route.Mode == "SERVAPP" && reg.test(route.Target)
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
45
go.mod
45
go.mod
|
@ -3,6 +3,7 @@ module github.com/azukaar/cosmos-server
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/docker/docker v23.0.1+incompatible
|
github.com/docker/docker v23.0.1+incompatible
|
||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
github.com/foomo/simplecert v1.8.4
|
github.com/foomo/simplecert v1.8.4
|
||||||
|
@ -12,13 +13,18 @@ require (
|
||||||
github.com/go-playground/validator/v10 v10.12.0
|
github.com/go-playground/validator/v10 v10.12.0
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/jasonlvhit/gocron v0.0.1
|
github.com/jasonlvhit/gocron v0.0.1
|
||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
|
||||||
|
github.com/ory/fosite v0.44.0
|
||||||
|
github.com/oschwald/geoip2-golang v1.8.0
|
||||||
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/roberthodgen/spa-server v0.0.0-20171007154335-bb87b4ff3253
|
github.com/roberthodgen/spa-server v0.0.0-20171007154335-bb87b4ff3253
|
||||||
github.com/shirou/gopsutil/v3 v3.23.3
|
github.com/shirou/gopsutil/v3 v3.23.3
|
||||||
go.deanishe.net/favicon v0.1.0
|
go.deanishe.net/favicon v0.1.0
|
||||||
go.mongodb.org/mongo-driver v1.11.3
|
go.mongodb.org/mongo-driver v1.11.3
|
||||||
golang.org/x/crypto v0.7.0
|
golang.org/x/crypto v0.7.0
|
||||||
|
golang.org/x/sys v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -34,29 +40,36 @@ require (
|
||||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||||
github.com/Azure/go-autorest/logger v0.2.0 // indirect
|
github.com/Azure/go-autorest/logger v0.2.0 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||||
github.com/Masterminds/semver v1.5.0 // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.6.0 // indirect
|
github.com/PuerkitoBio/goquery v1.6.0 // indirect
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.1 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.1 // indirect
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.869 // indirect
|
github.com/aliyun/alibaba-cloud-sdk-go v1.61.869 // indirect
|
||||||
github.com/andybalholm/cascadia v1.1.0 // 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.36.29 // indirect
|
github.com/aws/aws-sdk-go v1.36.29 // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
|
||||||
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.13.7 // indirect
|
github.com/cloudflare/cloudflare-go v0.13.7 // indirect
|
||||||
github.com/cpu/goacmedns v0.1.1 // indirect
|
github.com/cpu/goacmedns v0.1.1 // indirect
|
||||||
|
github.com/cristalhq/jwt/v4 v4.0.2 // indirect
|
||||||
|
github.com/dave/jennifer v1.4.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/deepmap/oapi-codegen v1.4.2 // indirect
|
github.com/deepmap/oapi-codegen v1.4.2 // indirect
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||||
github.com/dnsimple/dnsimple-go v0.63.0 // indirect
|
github.com/dnsimple/dnsimple-go v0.63.0 // indirect
|
||||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/ecordell/optgen v0.0.6 // indirect
|
||||||
github.com/exoscale/egoscale v0.40.0 // indirect
|
github.com/exoscale/egoscale v0.40.0 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
|
||||||
github.com/friendsofgo/errors v0.9.2 // indirect
|
github.com/friendsofgo/errors v0.9.2 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||||
github.com/go-acme/lego/v4 v4.1.3 // indirect
|
github.com/go-acme/lego/v4 v4.1.3 // indirect
|
||||||
github.com/go-errors/errors v1.1.1 // indirect
|
github.com/go-errors/errors v1.1.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
@ -64,8 +77,10 @@ require (
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.4.0 // indirect
|
github.com/go-resty/resty/v2 v2.4.0 // indirect
|
||||||
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
github.com/gofrs/uuid v4.0.0+incompatible // indirect
|
||||||
github.com/gogo/protobuf v1.2.0 // indirect
|
github.com/gogo/protobuf v1.2.1 // indirect
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||||
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.4.3 // indirect
|
github.com/golang/protobuf v1.4.3 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/goodhosts/hostsfile v0.0.7 // indirect
|
github.com/goodhosts/hostsfile v0.0.7 // indirect
|
||||||
|
@ -74,10 +89,11 @@ require (
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
|
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
|
||||||
github.com/gophercloud/gophercloud v0.15.0 // indirect
|
github.com/gophercloud/gophercloud v0.15.0 // indirect
|
||||||
github.com/gophercloud/utils v0.0.0-20210113034859-6f548432055a // indirect
|
github.com/gophercloud/utils v0.0.0-20210113034859-6f548432055a // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/jarcoal/httpmock v1.0.7 // indirect
|
github.com/jarcoal/httpmock v1.0.7 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.10 // indirect
|
github.com/json-iterator/go v1.1.10 // indirect
|
||||||
|
@ -89,14 +105,16 @@ require (
|
||||||
github.com/linode/linodego v0.24.2 // indirect
|
github.com/linode/linodego v0.24.2 // indirect
|
||||||
github.com/liquidweb/liquidweb-go v1.6.1 // indirect
|
github.com/liquidweb/liquidweb-go v1.6.1 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.1 // indirect
|
||||||
|
github.com/mattn/goveralls v0.0.6 // indirect
|
||||||
github.com/miekg/dns v1.1.35 // indirect
|
github.com/miekg/dns v1.1.35 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
|
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
|
||||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||||
github.com/nrdcg/auroradns v1.0.1 // indirect
|
github.com/nrdcg/auroradns v1.0.1 // indirect
|
||||||
github.com/nrdcg/desec v0.5.0 // indirect
|
github.com/nrdcg/desec v0.5.0 // indirect
|
||||||
|
@ -106,18 +124,28 @@ require (
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
|
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
|
||||||
github.com/oschwald/geoip2-golang v1.8.0 // indirect
|
github.com/ory/go-acc v0.2.6 // indirect
|
||||||
|
github.com/ory/go-convenience v0.1.0 // indirect
|
||||||
|
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/oschwald/maxminddb-golang v1.10.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.1.0 // indirect
|
github.com/ovh/go-ovh v1.1.0 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible // 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.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/pquerna/otp v1.4.0 // indirect
|
|
||||||
github.com/sacloud/libsacloud v1.36.2 // indirect
|
github.com/sacloud/libsacloud v1.36.2 // indirect
|
||||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||||
|
github.com/spf13/afero v1.3.2 // indirect
|
||||||
|
github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 // indirect
|
||||||
|
github.com/spf13/cobra v1.0.0 // indirect
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stretchr/objx v0.5.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/stretchr/testify v1.8.2 // indirect
|
github.com/stretchr/testify v1.8.2 // indirect
|
||||||
|
github.com/subosito/gotenv v1.2.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||||
github.com/transip/gotransip/v6 v6.5.0 // indirect
|
github.com/transip/gotransip/v6 v6.5.0 // indirect
|
||||||
|
@ -133,7 +161,6 @@ require (
|
||||||
golang.org/x/net v0.8.0 // indirect
|
golang.org/x/net v0.8.0 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 // indirect
|
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/text v0.8.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
|
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
|
@ -141,11 +168,11 @@ require (
|
||||||
google.golang.org/api v0.36.0 // indirect
|
google.golang.org/api v0.36.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc // indirect
|
google.golang.org/genproto v0.0.0-20210119180700-e258113e47cc // indirect
|
||||||
google.golang.org/grpc v1.35.0 // indirect
|
google.golang.org/grpc v1.36.0 // indirect
|
||||||
google.golang.org/protobuf v1.25.0 // indirect
|
google.golang.org/protobuf v1.25.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||||
gopkg.in/ns1/ns1-go.v2 v2.4.3 // indirect
|
gopkg.in/ns1/ns1-go.v2 v2.4.3 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.4.0 // indirect
|
gotest.tools/v3 v3.4.0 // indirect
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.5.12",
|
"version": "0.6.0-unstable",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "test-server.js",
|
"main": "test-server.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
11
readme.md
11
readme.md
|
@ -36,9 +36,10 @@ Cosmos is a self-hosted platform for running server applications securely and wi
|
||||||
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure them all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
|
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure them all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
|
||||||
|
|
||||||
* **Easy to use** 🚀👍 to install and use, with a simple web UI to manage your applications from any device
|
* **Easy to use** 🚀👍 to install and use, with a simple web UI to manage your applications from any device
|
||||||
|
* **powerful** 🧠🔥 Being easy does not mean being dumb: while Cosmos is easy to use, it is also powerful and flexible, you can even use it from the terminal if you want to!
|
||||||
* **User-friendly** 🧑🎨 For both new and experienced users: easily integrates into your existing home server, the already existing applications you have, and the new ones you want to install
|
* **User-friendly** 🧑🎨 For both new and experienced users: easily integrates into your existing home server, the already existing applications you have, and the new ones you want to install
|
||||||
* **SmartShield technology** 🧠🛡 Automatically secure your applications without manual adjustments (see below for more details)
|
* **SmartShield technology** 🧠🛡 Automatically secure your applications without manual adjustments (see below for more details)
|
||||||
* **Secure Authentication** 👦👩 Connect to all your applications with the same account, including **strong security** and **multi-factor authentication**
|
* **Secure Authentication** 👦👩 Connect to all your applications with the same account, including **strong security**, **multi-factor authentication** and **OpenId**
|
||||||
* **Latest Encryption Methods** 🔒🔑 To encrypt your data and protect your privacy. Security by design, and not as an afterthought
|
* **Latest Encryption Methods** 🔒🔑 To encrypt your data and protect your privacy. Security by design, and not as an afterthought
|
||||||
* **Reverse Proxy** 🔄🔗 Reverse Proxy included, with a UI to easily manage your applications and their settings
|
* **Reverse Proxy** 🔄🔗 Reverse Proxy included, with a UI to easily manage your applications and their settings
|
||||||
* **Automatic HTTPS** 🔑📜 certificates provisioning with Certbot / Let's Encrypt
|
* **Automatic HTTPS** 🔑📜 certificates provisioning with Certbot / Let's Encrypt
|
||||||
|
@ -53,6 +54,14 @@ And a **lot more planned features** are coming!
|
||||||
|
|
||||||
![schema](./schema.png)
|
![schema](./schema.png)
|
||||||
|
|
||||||
|
# What are the differences with other alternatives?
|
||||||
|
|
||||||
|
Cosmos has a few key differences with other alternatives such as YunoHost, Unraid, etc:
|
||||||
|
|
||||||
|
* **Security**: Cosmos has a unique strong focus on securing your application with exclusive features such as the smart-shield. It has 2FA, OpenID, anti-DDOS, and other security features built-in. It also has a strong focus on privacy, with the latest encryption methods and a strong focus on data protection. Unlike any other solutions, it assumes the software you run are not trustworthy, and protects you from them.
|
||||||
|
* **Power-user friendly**: Some of those alternatives can feel a bit "limiting" to someone who kows what they are doing. On the other hand, while Cosmos is designed to be easy to use, it is also powerful and flexible. It is designed to be used by both new and experienced users, and to integrate into your existing home server, the already existing applications you have, and the new ones you want to install. It can even be used from the terminal if you want to!
|
||||||
|
* **Flexible**: Unlike the alternatives, Cosmos is not exclusively focused around its app-store. Instead, it lets you freely install any application any way you want, and manage them from the UI, from Portainer, or from docker directly. Any of those applications will still be integrated into Cosmos and will also benefit from all the security features, Let's Encrypt, etc...
|
||||||
|
|
||||||
# What is the SmartShield?
|
# What is the SmartShield?
|
||||||
|
|
||||||
SmartShield is a modern API protection package designed to secure your API by implementing advanced rate-limiting and user restrictions. This helps efficiently allocate and protect your resources without manual adjustment of limits and policies.
|
SmartShield is a modern API protection package designed to secure your API by implementing advanced rate-limiting and user restrictions. This helps efficiently allocate and protect your resources without manual adjustment of limits and policies.
|
||||||
|
|
168
src/authorizationserver/oauth2.go
Normal file
168
src/authorizationserver/oauth2.go
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"github.com/ory/fosite"
|
||||||
|
"time"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ory/fosite/compose"
|
||||||
|
"github.com/ory/fosite/handler/openid"
|
||||||
|
"github.com/ory/fosite/storage"
|
||||||
|
"github.com/ory/fosite/token/jwt"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterHandlers(wellKnown *mux.Router, userRouter *mux.Router, serverRouter *mux.Router) {
|
||||||
|
// Set up oauth2 endpoints. You could also use gorilla/mux or any other router.
|
||||||
|
userRouter.HandleFunc("/auth", authEndpoint)
|
||||||
|
serverRouter.HandleFunc("/token", tokenEndpoint)
|
||||||
|
|
||||||
|
// user infos
|
||||||
|
serverRouter.HandleFunc("/userinfo", userInfosEndpoint)
|
||||||
|
|
||||||
|
// revoke tokens
|
||||||
|
serverRouter.HandleFunc("/revoke", revokeEndpoint)
|
||||||
|
serverRouter.HandleFunc("/introspect", introspectionEndpoint)
|
||||||
|
|
||||||
|
// public endpoints
|
||||||
|
wellKnown.HandleFunc("/openid-configuration", discoverEndpoint)
|
||||||
|
wellKnown.HandleFunc("/jwks.json", jwksEndpoint)
|
||||||
|
|
||||||
|
|
||||||
|
store.Clients["gitea"] = &fosite.DefaultClient{
|
||||||
|
ID: "gitea",
|
||||||
|
Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar"
|
||||||
|
RedirectURIs: []string{"http://localhost:3000/user/oauth2/Cosmos/callback"},
|
||||||
|
Scopes: []string{"openid", "email", "profile"},
|
||||||
|
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
|
||||||
|
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Clients["my-client"] = &fosite.DefaultClient{
|
||||||
|
ID: "my-client",
|
||||||
|
Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar"
|
||||||
|
RotatedSecrets: [][]byte{[]byte(`$2y$10$X51gLxUQJ.hGw1epgHTE5u0bt64xM0COU7K9iAp.OFg8p2pUd.1zC`)}, // = "foobaz",
|
||||||
|
RedirectURIs: []string{"http://localhost:3846/callback"},
|
||||||
|
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
|
||||||
|
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
|
||||||
|
Scopes: []string{"fosite", "openid", "photos", "offline"},
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Clients["minio"] = &fosite.DefaultClient{
|
||||||
|
ID: "minio",
|
||||||
|
Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar"
|
||||||
|
RedirectURIs: []string{"http://localhost:9090/oauth_callback"},
|
||||||
|
Scopes: []string{"openid", "email", "profile", "groups"},
|
||||||
|
ResponseTypes: []string{"id_token", "code", "token", "id_token token", "code id_token", "code token", "code id_token token"},
|
||||||
|
GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// fosite requires four parameters for the server to get up and running:
|
||||||
|
// 1. config - for any enforcement you may desire, you can do this using `compose.Config`. You like PKCE, enforce it!
|
||||||
|
// 2. store - no auth service is generally useful unless it can remember clients and users.
|
||||||
|
// fosite is incredibly composable, and the store parameter enables you to build and BYODb (Bring Your Own Database)
|
||||||
|
// 3. secret - required for code, access and refresh token generation.
|
||||||
|
// 4. privateKey - required for id/jwt token generation.
|
||||||
|
var (
|
||||||
|
// Check the api documentation of `compose.Config` for further configuration options.
|
||||||
|
config = &fosite.Config{
|
||||||
|
AccessTokenLifespan: time.Minute * 30,
|
||||||
|
GlobalSecret: secret,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the example storage that contains:
|
||||||
|
// * an OAuth2 Client with id "my-client" and secrets "foobar" and "foobaz" capable of all oauth2 and open id connect grant and response types.
|
||||||
|
// * a User for the resource owner password credentials grant type with username "peter" and password "secret".
|
||||||
|
//
|
||||||
|
// You will most likely replace this with your own logic once you set up a real world application.
|
||||||
|
store = storage.NewMemoryStore()
|
||||||
|
|
||||||
|
// This secret is used to sign authorize codes, access and refresh tokens.
|
||||||
|
// It has to be 32-bytes long for HMAC signing. This requirement can be configured via `compose.Config` above.
|
||||||
|
// In order to generate secure keys, the best thing to do is use crypto/rand:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "crypto/rand"
|
||||||
|
// "encoding/hex"
|
||||||
|
// "fmt"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// var secret = make([]byte, 32)
|
||||||
|
// _, err := rand.Read(secret)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// If you require this to key to be stable, for example, when running multiple fosite servers, you can generate the
|
||||||
|
// 32byte random key as above and push it out to a base64 encoded string.
|
||||||
|
// This can then be injected and decoded as the `var secret []byte` on server start.
|
||||||
|
secret = []byte("some-cool-secret-that-is-32bytes")
|
||||||
|
|
||||||
|
// privateKey is used to sign JWT tokens. The default strategy uses RS256 (RSA Signature with SHA-256)
|
||||||
|
AuthPrivateKey, _ = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Build a fosite instance with all OAuth2 and OpenID Connect handlers enabled, plugging in our configurations as specified above.
|
||||||
|
var oauth2 = compose.ComposeAllEnabled(config, store, AuthPrivateKey)
|
||||||
|
|
||||||
|
// A session is passed from the `/auth` to the `/token` endpoint. You probably want to store data like: "Who made the request",
|
||||||
|
// "What organization does that person belong to" and so on.
|
||||||
|
// For our use case, the session will meet the requirements imposed by JWT access tokens, HMAC access tokens and OpenID Connect
|
||||||
|
// ID Tokens plus a custom field
|
||||||
|
|
||||||
|
// newSession is a helper function for creating a new session. This may look like a lot of code but since we are
|
||||||
|
// setting up multiple strategies it is a bit longer.
|
||||||
|
// Usually, you could do:
|
||||||
|
//
|
||||||
|
// session = new(fosite.DefaultSession)
|
||||||
|
func newSession(user string, req *http.Request) *openid.DefaultSession {
|
||||||
|
// get hostname from request
|
||||||
|
hostname := req.Host
|
||||||
|
|
||||||
|
// external request
|
||||||
|
if hostname == utils.GetMainConfig().HTTPConfig.Hostname {
|
||||||
|
if utils.IsHTTPS {
|
||||||
|
hostname = "https://" + hostname
|
||||||
|
} else {
|
||||||
|
hostname = "http://" + hostname
|
||||||
|
}
|
||||||
|
} else if hostname == os.Getenv("HOSTNAME") {
|
||||||
|
hostname = "http://" + hostname
|
||||||
|
} else {
|
||||||
|
utils.Error("Invalid hostname for OpenID request: " + hostname, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Issuer: hostname,
|
||||||
|
Subject: user,
|
||||||
|
// Audience: []string{"https://my-client.my-application.com"},
|
||||||
|
ExpiresAt: time.Now().Add(time.Hour * 6),
|
||||||
|
IssuedAt: time.Now(),
|
||||||
|
RequestedAt: time.Now(),
|
||||||
|
AuthTime: time.Now(),
|
||||||
|
},
|
||||||
|
Headers: &jwt.Headers{
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"kid": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
82
src/authorizationserver/oauth2_auth.go
Normal file
82
src/authorizationserver/oauth2_auth.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// This context will be passed to all methods.
|
||||||
|
ctx := req.Context()
|
||||||
|
|
||||||
|
|
||||||
|
if utils.LoggedInOnly(rw, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nickname := req.Header.Get("x-cosmos-user")
|
||||||
|
|
||||||
|
hostname := utils.GetMainConfig().HTTPConfig.Hostname
|
||||||
|
if utils.IsHTTPS {
|
||||||
|
hostname = "https://" + hostname
|
||||||
|
} else {
|
||||||
|
hostname = "http://" + hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's create an AuthorizeRequest object!
|
||||||
|
// It will analyze the request and extract important information like scopes, response type and others.
|
||||||
|
ar, err := oauth2.NewAuthorizeRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error occurred in NewAuthorizeRequest: %+v", err)
|
||||||
|
oauth2.WriteAuthorizeError(ctx, rw, ar, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// let's see what scopes the user gave consent to
|
||||||
|
for _, scope := range req.PostForm["scopes"] {
|
||||||
|
ar.GrantScope(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the user is authorized, we set up a session:
|
||||||
|
mySessionData := newSession(nickname, req)
|
||||||
|
|
||||||
|
// When using the HMACSHA strategy you must use something that implements the HMACSessionContainer.
|
||||||
|
// It brings you the power of overriding the default values.
|
||||||
|
//
|
||||||
|
// mySessionData.HMACSession = &strategy.HMACSession{
|
||||||
|
// AccessTokenExpiry: time.Now().Add(time.Day),
|
||||||
|
// AuthorizeCodeExpiry: time.Now().Add(time.Day),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|
||||||
|
// If you're using the JWT strategy, there's currently no distinction between access token and authorize code claims.
|
||||||
|
// Therefore, you both access token and authorize code will have the same "exp" claim. If this is something you
|
||||||
|
// need let us know on github.
|
||||||
|
//
|
||||||
|
// mySessionData.JWTClaims.ExpiresAt = time.Now().Add(time.Day)
|
||||||
|
|
||||||
|
// It's also wise to check the requested scopes, e.g.:
|
||||||
|
// if ar.GetRequestedScopes().Has("admin") {
|
||||||
|
// http.Error(rw, "you're not allowed to do that", http.StatusForbidden)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Now we need to get a response. This is the place where the AuthorizeEndpointHandlers kick in and start processing the request.
|
||||||
|
// NewAuthorizeResponse is capable of running multiple response type handlers which in turn enables this library
|
||||||
|
// to support open id connect.
|
||||||
|
response, err := oauth2.NewAuthorizeResponse(ctx, ar, mySessionData)
|
||||||
|
|
||||||
|
// Catch any errors, e.g.:
|
||||||
|
// * unknown client
|
||||||
|
// * invalid redirect
|
||||||
|
// * ...
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error occurred in NewAuthorizeResponse: %+v", err)
|
||||||
|
oauth2.WriteAuthorizeError(ctx, rw, ar, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last but not least, send the response!
|
||||||
|
oauth2.WriteAuthorizeResponse(ctx, rw, ar, response)
|
||||||
|
}
|
268
src/authorizationserver/oauth2_discover.go
Normal file
268
src/authorizationserver/oauth2_discover.go
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// OpenID Connect Discovery Metadata
|
||||||
|
//
|
||||||
|
// Includes links to several endpoints (for example `/oauth2/token`) and exposes information on supported signature algorithms
|
||||||
|
// among others.
|
||||||
|
//
|
||||||
|
// swagger:model oidcConfiguration
|
||||||
|
type oidcConfiguration struct {
|
||||||
|
// OpenID Connect Issuer URL
|
||||||
|
//
|
||||||
|
// An URL using the https scheme with no query or fragment component that the OP asserts as its IssuerURL Identifier.
|
||||||
|
// If IssuerURL discovery is supported , this value MUST be identical to the issuer value returned
|
||||||
|
// by WebFinger. This also MUST be identical to the iss Claim value in ID Tokens issued from this IssuerURL.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: https://playground.ory.sh/ory-hydra/public/
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Authorization Endpoint URL
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: https://playground.ory.sh/ory-hydra/public/oauth2/auth
|
||||||
|
AuthURL string `json:"authorization_endpoint"`
|
||||||
|
|
||||||
|
// OpenID Connect Dynamic Client Registration Endpoint URL
|
||||||
|
//
|
||||||
|
// example: https://playground.ory.sh/ory-hydra/admin/client
|
||||||
|
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Token Endpoint URL
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: https://playground.ory.sh/ory-hydra/public/oauth2/token
|
||||||
|
TokenURL string `json:"token_endpoint"`
|
||||||
|
|
||||||
|
// OpenID Connect Well-Known JSON Web Keys URL
|
||||||
|
//
|
||||||
|
// URL of the OP's JSON Web Key Set [JWK] document. This contains the signing key(s) the RP uses to validate
|
||||||
|
// signatures from the OP. The JWK Set MAY also contain the Server's encryption key(s), which are used by RPs
|
||||||
|
// to encrypt requests to the Server. When both signing and encryption keys are made available, a use (Key Use)
|
||||||
|
// parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage.
|
||||||
|
// Although some algorithms allow the same key to be used for both signatures and encryption, doing so is
|
||||||
|
// NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide X.509 representations of
|
||||||
|
// keys provided. When used, the bare key values MUST still be present and MUST match those in the certificate.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example: https://{slug}.projects.oryapis.com/.well-known/jwks.json
|
||||||
|
JWKsURI string `json:"jwks_uri"`
|
||||||
|
|
||||||
|
// OpenID Connect Supported Subject Types
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the Subject Identifier types that this OP supports. Valid types include
|
||||||
|
// pairwise and public.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
// example:
|
||||||
|
// - public
|
||||||
|
// - pairwise
|
||||||
|
SubjectTypes []string `json:"subject_types_supported"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Supported Response Types
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the OAuth 2.0 response_type values that this OP supports. Dynamic OpenID
|
||||||
|
// Providers MUST support the code, id_token, and the token id_token Response Type values.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
ResponseTypes []string `json:"response_types_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Supported Claims
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the Claim Names of the Claims that the OpenID Provider MAY be able to supply
|
||||||
|
// values for. Note that for privacy or other reasons, this might not be an exhaustive list.
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Supported Grant Types
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the OAuth 2.0 Grant Type values that this OP supports.
|
||||||
|
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Supported Response Modes
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the OAuth 2.0 response_mode values that this OP supports.
|
||||||
|
ResponseModesSupported []string `json:"response_modes_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Userinfo URL
|
||||||
|
//
|
||||||
|
// URL of the OP's UserInfo Endpoint.
|
||||||
|
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Supported Scope Values
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports. The server MUST
|
||||||
|
// support the openid scope value. Servers MAY choose not to advertise some supported scope values even when this parameter is used
|
||||||
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Supported Client Authentication Methods
|
||||||
|
//
|
||||||
|
// JSON array containing a list of Client Authentication methods supported by this Token Endpoint. The options are
|
||||||
|
// client_secret_post, client_secret_basic, client_secret_jwt, and private_key_jwt, as described in Section 9 of OpenID Connect Core 1.0
|
||||||
|
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Supported Userinfo Signing Algorithm
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the JWS [JWS] signing algorithms (alg values) [JWA] supported by the UserInfo Endpoint to encode the Claims in a JWT [JWT].
|
||||||
|
UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Supported ID Token Signing Algorithms
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the ID Token
|
||||||
|
// to encode the Claims in a JWT.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Default ID Token Signing Algorithms
|
||||||
|
//
|
||||||
|
// Algorithm used to sign OpenID Connect ID Tokens.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
IDTokenSignedResponseAlg []string `json:"id_token_signed_response_alg"`
|
||||||
|
|
||||||
|
// OpenID Connect User Userinfo Signing Algorithm
|
||||||
|
//
|
||||||
|
// Algorithm used to sign OpenID Connect Userinfo Responses.
|
||||||
|
//
|
||||||
|
// required: true
|
||||||
|
UserinfoSignedResponseAlg []string `json:"userinfo_signed_response_alg"`
|
||||||
|
|
||||||
|
// OpenID Connect Request Parameter Supported
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP supports use of the request parameter, with true indicating support.
|
||||||
|
RequestParameterSupported bool `json:"request_parameter_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Request URI Parameter Supported
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP supports use of the request_uri parameter, with true indicating support.
|
||||||
|
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Requires Request URI Registration
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP requires any request_uri values used to be pre-registered
|
||||||
|
// using the request_uris registration parameter.
|
||||||
|
RequireRequestURIRegistration bool `json:"require_request_uri_registration"`
|
||||||
|
|
||||||
|
// OpenID Connect Claims Parameter Parameter Supported
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP supports use of the claims parameter, with true indicating support.
|
||||||
|
ClaimsParameterSupported bool `json:"claims_parameter_supported"`
|
||||||
|
|
||||||
|
// OAuth 2.0 Token Revocation URL
|
||||||
|
//
|
||||||
|
// URL of the authorization server's OAuth 2.0 revocation endpoint.
|
||||||
|
RevocationEndpoint string `json:"revocation_endpoint"`
|
||||||
|
|
||||||
|
// OpenID Connect Back-Channel Logout Supported
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP supports back-channel logout, with true indicating support.
|
||||||
|
BackChannelLogoutSupported bool `json:"backchannel_logout_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Back-Channel Logout Session Required
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP can pass a sid (session ID) Claim in the Logout Token to identify the RP
|
||||||
|
// session with the OP. If supported, the sid Claim is also included in ID Tokens issued by the OP
|
||||||
|
BackChannelLogoutSessionSupported bool `json:"backchannel_logout_session_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Front-Channel Logout Supported
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP supports HTTP-based logout, with true indicating support.
|
||||||
|
FrontChannelLogoutSupported bool `json:"frontchannel_logout_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect Front-Channel Logout Session Required
|
||||||
|
//
|
||||||
|
// Boolean value specifying whether the OP can pass iss (issuer) and sid (session ID) query parameters to identify
|
||||||
|
// the RP session with the OP when the frontchannel_logout_uri is used. If supported, the sid Claim is also
|
||||||
|
// included in ID Tokens issued by the OP.
|
||||||
|
FrontChannelLogoutSessionSupported bool `json:"frontchannel_logout_session_supported"`
|
||||||
|
|
||||||
|
// OpenID Connect End-Session Endpoint
|
||||||
|
//
|
||||||
|
// URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP.
|
||||||
|
EndSessionEndpoint string `json:"end_session_endpoint"`
|
||||||
|
|
||||||
|
// OpenID Connect Supported Request Object Signing Algorithms
|
||||||
|
//
|
||||||
|
// JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for Request Objects,
|
||||||
|
// which are described in Section 6.1 of OpenID Connect Core 1.0 [OpenID.Core]. These algorithms are used both when
|
||||||
|
// the Request Object is passed by value (using the request parameter) and when it is passed by reference
|
||||||
|
// (using the request_uri parameter).
|
||||||
|
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"`
|
||||||
|
|
||||||
|
// OAuth 2.0 PKCE Supported Code Challenge Methods
|
||||||
|
//
|
||||||
|
// JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported
|
||||||
|
// by this authorization server.
|
||||||
|
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func discoverEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// get hostname from request
|
||||||
|
hostname := req.Host
|
||||||
|
|
||||||
|
realHostname := utils.GetMainConfig().HTTPConfig.Hostname
|
||||||
|
if utils.IsHTTPS {
|
||||||
|
realHostname = "https://" + realHostname
|
||||||
|
} else {
|
||||||
|
realHostname = "http://" + realHostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// external request
|
||||||
|
if hostname == utils.GetMainConfig().HTTPConfig.Hostname {
|
||||||
|
if utils.IsHTTPS {
|
||||||
|
hostname = "https://" + hostname
|
||||||
|
} else {
|
||||||
|
hostname = "http://" + hostname
|
||||||
|
}
|
||||||
|
} else if hostname == os.Getenv("HOSTNAME") {
|
||||||
|
hostname = "http://" + hostname
|
||||||
|
} else {
|
||||||
|
utils.Error(fmt.Sprintf("invalid hostname for OpenID: %s expecting %s or %s", hostname, utils.GetMainConfig().HTTPConfig.Hostname, os.Getenv("HOSTNAME")), nil)
|
||||||
|
http.Error(rw, "invalid hostname for OpenID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
json.NewEncoder(rw).Encode(&oidcConfiguration{
|
||||||
|
Issuer: hostname,
|
||||||
|
AuthURL: realHostname + "/ui/openid",
|
||||||
|
TokenURL: hostname + "/oauth2/token",
|
||||||
|
JWKsURI: hostname + "/.well-known/jwks.json",
|
||||||
|
RevocationEndpoint: hostname + "/oauth2/revoke",
|
||||||
|
UserinfoEndpoint: hostname + "/oauth2/userinfo",
|
||||||
|
// RegistrationEndpoint: hostname + "/oauth2/register",
|
||||||
|
SubjectTypes: []string{"public", "pairwise"},
|
||||||
|
ResponseTypes: []string{"code", "code id_token", "id_token", "token id_token", "token", "token id_token code"},
|
||||||
|
ClaimsSupported: []string{"aud", "email", "email_verified", "exp", "iat", "iss", "locale", "name", "sub"},
|
||||||
|
ScopesSupported: []string{"openid", "offline", "profile", "email", "address", "phone"},
|
||||||
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic", "private_key_jwt", "none"},
|
||||||
|
// IDTokenSigningAlgValuesSupported: []string{key.Algorithm},
|
||||||
|
// IDTokenSignedResponseAlg: []string{key.Algorithm},
|
||||||
|
// UserinfoSignedResponseAlg: []string{key.Algorithm},
|
||||||
|
GrantTypesSupported: []string{"authorization_code", "implicit", "client_credentials", "refresh_token"},
|
||||||
|
ResponseModesSupported: []string{"query", "fragment"},
|
||||||
|
// UserinfoSigningAlgValuesSupported: []string{"none", key.Algorithm},
|
||||||
|
RequestParameterSupported: true,
|
||||||
|
RequestURIParameterSupported: true,
|
||||||
|
RequireRequestURIRegistration: true,
|
||||||
|
BackChannelLogoutSupported: true,
|
||||||
|
BackChannelLogoutSessionSupported: true,
|
||||||
|
FrontChannelLogoutSupported: true,
|
||||||
|
FrontChannelLogoutSessionSupported: true,
|
||||||
|
EndSessionEndpoint: hostname + "/ui/logout",
|
||||||
|
RequestObjectSigningAlgValuesSupported: []string{"RS256"},
|
||||||
|
CodeChallengeMethodsSupported: []string{"plain", "S256"},
|
||||||
|
})
|
||||||
|
}
|
19
src/authorizationserver/oauth2_introspect.go
Normal file
19
src/authorizationserver/oauth2_introspect.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func introspectionEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx := req.Context()
|
||||||
|
mySessionData := newSession("", req)
|
||||||
|
ir, err := oauth2.NewIntrospectionRequest(ctx, req, mySessionData)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error occurred in NewIntrospectionRequest: %+v", err)
|
||||||
|
oauth2.WriteIntrospectionError(ctx, rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2.WriteIntrospectionResponse(ctx, rw, ir)
|
||||||
|
}
|
50
src/authorizationserver/oauth2_jwks.go
Normal file
50
src/authorizationserver/oauth2_jwks.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"encoding/base64"
|
||||||
|
"math/big"
|
||||||
|
"crypto/rsa"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JsonWebKey struct {
|
||||||
|
Alg string `json:"alg"`
|
||||||
|
Kid string `json:"kid"`
|
||||||
|
Kty string `json:"kty"`
|
||||||
|
Use string `json:"use"`
|
||||||
|
N string `json:"n"`
|
||||||
|
E string `json:"e"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonWebKeySet struct {
|
||||||
|
Keys []JsonWebKey `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func jwksEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
hostname := utils.GetMainConfig().HTTPConfig.Hostname
|
||||||
|
|
||||||
|
if utils.IsHTTPS {
|
||||||
|
hostname = "https://" + hostname
|
||||||
|
} else {
|
||||||
|
hostname = "http://" + hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSA Public Key from rsa.GenerateKey
|
||||||
|
publicKey := AuthPrivateKey.Public().(*rsa.PublicKey)
|
||||||
|
|
||||||
|
json.NewEncoder(rw).Encode(&JsonWebKeySet{
|
||||||
|
Keys: []JsonWebKey{
|
||||||
|
{
|
||||||
|
Alg: "RS256",
|
||||||
|
Kid: "1",
|
||||||
|
Kty: "RSA",
|
||||||
|
Use: "sig",
|
||||||
|
N: base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()),
|
||||||
|
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
16
src/authorizationserver/oauth2_revoke.go
Normal file
16
src/authorizationserver/oauth2_revoke.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func revokeEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// This context will be passed to all methods.
|
||||||
|
ctx := req.Context()
|
||||||
|
|
||||||
|
// This will accept the token revocation request and validate various parameters.
|
||||||
|
err := oauth2.NewRevocationRequest(ctx, req)
|
||||||
|
|
||||||
|
// All done, send the response.
|
||||||
|
oauth2.WriteRevocationResponse(ctx, rw, err)
|
||||||
|
}
|
53
src/authorizationserver/oauth2_token.go
Normal file
53
src/authorizationserver/oauth2_token.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
// "fmt"
|
||||||
|
|
||||||
|
// "github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tokenEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
// This context will be passed to all methods.
|
||||||
|
ctx := req.Context()
|
||||||
|
|
||||||
|
// Create an empty session object which will be passed to the request handlers
|
||||||
|
mySessionData := newSession("", req)
|
||||||
|
|
||||||
|
// This will create an access request object and iterate through the registered TokenEndpointHandlers to validate the request.
|
||||||
|
accessRequest, err := oauth2.NewAccessRequest(ctx, req, mySessionData)
|
||||||
|
|
||||||
|
// Catch any errors, e.g.:
|
||||||
|
// * unknown client
|
||||||
|
// * invalid redirect
|
||||||
|
// * ...
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error occurred in NewAccessRequest: %+v", err)
|
||||||
|
oauth2.WriteAccessError(ctx, rw, accessRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a client_credentials grant, grant all requested scopes
|
||||||
|
// NewAccessRequest validated that all requested scopes the client is allowed to perform
|
||||||
|
// based on configured scope matching strategy.
|
||||||
|
if accessRequest.GetGrantTypes().ExactOne("client_credentials") {
|
||||||
|
for _, scope := range accessRequest.GetRequestedScopes() {
|
||||||
|
accessRequest.GrantScope(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next we create a response for the access request. Again, we iterate through the TokenEndpointHandlers
|
||||||
|
// and aggregate the result in response.
|
||||||
|
response, err := oauth2.NewAccessResponse(ctx, accessRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error occurred in NewAccessResponse: %+v", err)
|
||||||
|
oauth2.WriteAccessError(ctx, rw, accessRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// All done, send the response.
|
||||||
|
oauth2.WriteAccessResponse(ctx, rw, accessRequest, response)
|
||||||
|
|
||||||
|
// The client now has a valid access token
|
||||||
|
}
|
85
src/authorizationserver/oauth2_user.go
Normal file
85
src/authorizationserver/oauth2_user.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package authorizationserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"github.com/ory/fosite"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ory/fosite/handler/openid"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
type oidcUser struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
IssuedAt int64 `json:"iat"`
|
||||||
|
ExpiresAt int64 `json:"exp"`
|
||||||
|
Issuer string `json:"iss"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func userInfosEndpoint(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
ctx := req.Context()
|
||||||
|
mySessionData := newSession("", req)
|
||||||
|
tokenType, ar, err := oauth2.IntrospectToken(ctx, fosite.AccessTokenFromRequest(req), fosite.AccessToken, mySessionData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("Error occurred in NewIntrospectionRequest: %+v", err)
|
||||||
|
oauth2.WriteIntrospectionError(ctx, rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenType != fosite.AccessToken {
|
||||||
|
errorDescription := "Only access tokens are allowed in the authorization header."
|
||||||
|
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Bearer error="invalid_token",error_description="%s"`, errorDescription))
|
||||||
|
// h.r.Writer().WriteErrorCode(w, r, http.StatusUnauthorized, errors.New(errorDescription))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interim := ar.GetSession().(*openid.DefaultSession).IDTokenClaims().ToMap()
|
||||||
|
|
||||||
|
nickname := interim["sub"].(string)
|
||||||
|
|
||||||
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(rw, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Debug("UserGet: Get user " + nickname)
|
||||||
|
|
||||||
|
user := utils.User{}
|
||||||
|
|
||||||
|
err = c.FindOne(nil, map[string]interface{}{
|
||||||
|
"Nickname": nickname,
|
||||||
|
}).Decode(&user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("UserGet: Error while getting user", err)
|
||||||
|
utils.HTTPError(rw, "User Get Error", http.StatusInternalServerError, "UD001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseToken := &oidcUser{
|
||||||
|
Name: interim["sub"].(string),
|
||||||
|
Username: interim["sub"].(string),
|
||||||
|
Nickname: interim["sub"].(string),
|
||||||
|
Subject: interim["sub"].(string),
|
||||||
|
IssuedAt: interim["iat"].(int64),
|
||||||
|
ExpiresAt: interim["exp"].(int64),
|
||||||
|
Issuer: interim["iss"].(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
// check scopes has email
|
||||||
|
if ar.GetGrantedScopes().Has("email") {
|
||||||
|
baseToken.Email = user.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(rw).Encode(baseToken)
|
||||||
|
}
|
85
src/dns.go
Normal file
85
src/dns.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckDNSRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.LoggedInOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.Method == "GET") {
|
||||||
|
url := utils.SanitizeSafe(req.URL.Query().Get("url"))
|
||||||
|
url = strings.Split(url, ":")[0]
|
||||||
|
|
||||||
|
if url == "" {
|
||||||
|
utils.Error("CheckDNS", nil)
|
||||||
|
utils.HTTPError(w, "Internal server error: No URL requested", http.StatusInternalServerError, "DNS001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errDNS := utils.CheckDNS(url)
|
||||||
|
|
||||||
|
if errDNS != nil {
|
||||||
|
utils.Error("CheckDNS", errDNS)
|
||||||
|
utils.HTTPError(w, "DNS Check error: " + errDNS.Error(), http.StatusInternalServerError, "DNS002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("CheckDNS: Method not allowed" + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func GetDNSRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if !utils.GetMainConfig().NewInstall && (utils.LoggedInOnly(w, req) != nil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.Method == "GET") {
|
||||||
|
url := utils.SanitizeSafe(req.URL.Query().Get("url"))
|
||||||
|
url = strings.Split(url, ":")[0]
|
||||||
|
|
||||||
|
if url == "" {
|
||||||
|
utils.Error("CheckDNS", nil)
|
||||||
|
utils.HTTPError(w, "Internal server error: No URL requested", http.StatusInternalServerError, "DNS001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, err := net.LookupIP(url)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("CheckDNS", err)
|
||||||
|
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DNS001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"data": ip.String(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Error("CheckDNS: No DNS entry found. Did you point the domain to your server?", nil)
|
||||||
|
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DNS001")
|
||||||
|
} else {
|
||||||
|
utils.Error("CheckDNS: Method not allowed" + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/azukaar/cosmos-server/src/configapi"
|
"github.com/azukaar/cosmos-server/src/configapi"
|
||||||
"github.com/azukaar/cosmos-server/src/proxy"
|
"github.com/azukaar/cosmos-server/src/proxy"
|
||||||
"github.com/azukaar/cosmos-server/src/docker"
|
"github.com/azukaar/cosmos-server/src/docker"
|
||||||
|
"github.com/azukaar/cosmos-server/src/authorizationserver"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -66,8 +67,14 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
|
|
||||||
// redirect http to https
|
// redirect http to https
|
||||||
go (func () {
|
go (func () {
|
||||||
// err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, http.HandlerFunc(simplecert.Redirect))
|
httpRouter := mux.NewRouter()
|
||||||
err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
// add support for internal OpenID requests
|
||||||
|
// if os.Getenv("HOSTNAME") != "" {
|
||||||
|
// authorizationserver.RegisterHandlers(httpRouter.Host(os.Getenv("HOSTNAME")).Subrouter())
|
||||||
|
// }
|
||||||
|
|
||||||
|
httpRouter.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// change port in host
|
// change port in host
|
||||||
if strings.HasSuffix(r.Host, ":" + serverPortHTTP) {
|
if strings.HasSuffix(r.Host, ":" + serverPortHTTP) {
|
||||||
if serverPortHTTPS != "443" {
|
if serverPortHTTPS != "443" {
|
||||||
|
@ -78,7 +85,10 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently)
|
http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently)
|
||||||
}))
|
})
|
||||||
|
|
||||||
|
// err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, http.HandlerFunc(simplecert.Redirect))
|
||||||
|
err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, httpRouter)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Listening to HTTP (Redirecting to HTTPS)", err)
|
utils.Fatal("Listening to HTTP (Redirecting to HTTPS)", err)
|
||||||
|
@ -143,6 +153,31 @@ func tokenMiddleware(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SecureAPI(userRouter *mux.Router, public bool) {
|
||||||
|
if(!public) {
|
||||||
|
userRouter.Use(tokenMiddleware)
|
||||||
|
}
|
||||||
|
userRouter.Use(proxy.SmartShieldMiddleware(
|
||||||
|
utils.SmartShieldPolicy{
|
||||||
|
Enabled: true,
|
||||||
|
PolicyStrictness: 1,
|
||||||
|
PerUserRequestLimit: 5000,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
userRouter.Use(utils.MiddlewareTimeout(45 * time.Second))
|
||||||
|
userRouter.Use(utils.BlockPostWithoutReferer)
|
||||||
|
userRouter.Use(proxy.BotDetectionMiddleware)
|
||||||
|
userRouter.Use(httprate.Limit(60, 1*time.Minute,
|
||||||
|
httprate.WithKeyFuncs(httprate.KeyByIP),
|
||||||
|
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
utils.Error("Too many requests. Throttling", nil)
|
||||||
|
utils.HTTPError(w, "Too many requests",
|
||||||
|
http.StatusTooManyRequests, "HTTP003")
|
||||||
|
return
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
func StartServer() {
|
func StartServer() {
|
||||||
baseMainConfig := utils.GetBaseMainConfig()
|
baseMainConfig := utils.GetBaseMainConfig()
|
||||||
config := utils.GetMainConfig()
|
config := utils.GetMainConfig()
|
||||||
|
@ -200,6 +235,9 @@ func StartServer() {
|
||||||
|
|
||||||
srapi := router.PathPrefix("/cosmos").Subrouter()
|
srapi := router.PathPrefix("/cosmos").Subrouter()
|
||||||
|
|
||||||
|
srapi.HandleFunc("/api/dns", GetDNSRoute)
|
||||||
|
srapi.HandleFunc("/api/dns-check", CheckDNSRoute)
|
||||||
|
|
||||||
srapi.HandleFunc("/api/status", StatusRoute)
|
srapi.HandleFunc("/api/status", StatusRoute)
|
||||||
srapi.HandleFunc("/api/can-send-email", CanSendEmail)
|
srapi.HandleFunc("/api/can-send-email", CanSendEmail)
|
||||||
srapi.HandleFunc("/api/favicon", GetFavicon)
|
srapi.HandleFunc("/api/favicon", GetFavicon)
|
||||||
|
@ -242,30 +280,13 @@ func StartServer() {
|
||||||
|
|
||||||
srapi.HandleFunc("/api/docker-service", docker.CreateServiceRoute)
|
srapi.HandleFunc("/api/docker-service", docker.CreateServiceRoute)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||||
srapi.Use(utils.EnsureHostname)
|
srapi.Use(utils.EnsureHostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
srapi.Use(tokenMiddleware)
|
SecureAPI(srapi, false)
|
||||||
srapi.Use(proxy.SmartShieldMiddleware(
|
|
||||||
utils.SmartShieldPolicy{
|
|
||||||
Enabled: true,
|
|
||||||
PolicyStrictness: 1,
|
|
||||||
PerUserRequestLimit: 5000,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
srapi.Use(utils.MiddlewareTimeout(45 * time.Second))
|
|
||||||
srapi.Use(utils.BlockPostWithoutReferer)
|
|
||||||
srapi.Use(proxy.BotDetectionMiddleware)
|
|
||||||
srapi.Use(httprate.Limit(120, 1*time.Minute,
|
|
||||||
httprate.WithKeyFuncs(httprate.KeyByIP),
|
|
||||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
utils.Error("Too many requests. Throttling", nil)
|
|
||||||
utils.HTTPError(w, "Too many requests",
|
|
||||||
http.StatusTooManyRequests, "HTTP003")
|
|
||||||
return
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
|
|
||||||
pwd,_ := os.Getwd()
|
pwd,_ := os.Getwd()
|
||||||
utils.Log("Starting in " + pwd)
|
utils.Log("Starting in " + pwd)
|
||||||
|
@ -288,6 +309,18 @@ func StartServer() {
|
||||||
http.Redirect(w, r, "/ui", http.StatusMovedPermanently)
|
http.Redirect(w, r, "/ui", http.StatusMovedPermanently)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
userRouter := router.PathPrefix("/oauth2").Subrouter()
|
||||||
|
SecureAPI(userRouter, false)
|
||||||
|
|
||||||
|
serverRouter := router.PathPrefix("/oauth2").Subrouter()
|
||||||
|
SecureAPI(userRouter, true)
|
||||||
|
|
||||||
|
wellKnownRouter := router.PathPrefix("/.well-known").Subrouter()
|
||||||
|
SecureAPI(userRouter, true)
|
||||||
|
|
||||||
|
authorizationserver.RegisterHandlers(wellKnownRouter, userRouter, serverRouter)
|
||||||
|
|
||||||
if ((HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["SELFSIGNED"] || HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["PROVIDED"]) &&
|
if ((HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["SELFSIGNED"] || HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["PROVIDED"]) &&
|
||||||
tlsCert != "" && tlsKey != "") || (HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
|
tlsCert != "" && tlsKey != "") || (HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
|
||||||
utils.Log("TLS certificate exist, starting HTTPS servers and redirecting HTTP to HTTPS")
|
utils.Log("TLS certificate exist, starting HTTPS servers and redirecting HTTP to HTTPS")
|
||||||
|
|
|
@ -23,7 +23,7 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err
|
||||||
// if new install
|
// if new install
|
||||||
if config.NewInstall {
|
if config.NewInstall {
|
||||||
// check route
|
// check route
|
||||||
if req.URL.Path != "/cosmos/api/status" && req.URL.Path != "/cosmos/api/newInstall" {
|
if req.URL.Path != "/cosmos/api/status" && req.URL.Path != "/cosmos/api/newInstall" && req.URL.Path != "/cosmos/api/dns" {
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"status": "NEW_INSTALL",
|
"status": "NEW_INSTALL",
|
||||||
})
|
})
|
||||||
|
|
52
src/utils/dns.go
Normal file
52
src/utils/dns.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
// "fmt"
|
||||||
|
// "os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckDNS(url string) error {
|
||||||
|
Log("CheckDNS: " + url)
|
||||||
|
|
||||||
|
realHostname := GetMainConfig().HTTPConfig.Hostname
|
||||||
|
realHostname = strings.Split(realHostname, ":")[0]
|
||||||
|
|
||||||
|
|
||||||
|
ips, err := net.LookupIP(url)
|
||||||
|
ipsReal, errReal := net.LookupIP(realHostname)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errReal != nil {
|
||||||
|
return errReal
|
||||||
|
}
|
||||||
|
|
||||||
|
ipCheck := ""
|
||||||
|
ipReal := ""
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
// if IPV4
|
||||||
|
if ip.To4() != nil {
|
||||||
|
ipCheck = ip.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ipsReal {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
ipReal = ip.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipCheck != ipReal {
|
||||||
|
return errors.New("DNS mismatch, this endpoint does not seem to point to your server IP: " + ipCheck + " != " + ipReal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue