[release] version 0.6.0-unstable

This commit is contained in:
Yann Stepienik 2023-06-04 15:41:26 +01:00
parent 0cdb11ba82
commit 60bf7627bb
29 changed files with 2165 additions and 62 deletions

View file

@ -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)

View file

@ -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
}; };

View file

@ -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;
} }
} }
}) })

View 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;

View file

@ -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));
}} }}

View file

@ -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,9 +32,21 @@ 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"

View file

@ -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])}

View file

@ -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">

View file

@ -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">

View file

@ -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);

View file

@ -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 />

View file

@ -5,4 +5,24 @@ export const randomString = (length) => {
for (let i = 0; i < length; i++) for (let i = 0; i < length; i++)
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;
} }

View file

@ -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
View file

@ -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

923
go.sum

File diff suppressed because it is too large Load diff

View file

@ -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": {

View file

@ -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.

View 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",
},
},
}
}

View 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)
}

View 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"},
})
}

View 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)
}

View 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()),
},
},
})
}

View 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)
}

View 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
}

View 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
View 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
}
}

View file

@ -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)
@ -241,31 +279,14 @@ func StartServer() {
srapi.HandleFunc("/api/servapps", docker.ContainersRoute) srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
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")

View file

@ -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
View 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
}