This commit is contained in:
Simon Larsen
2022-12-09 08:19:51 +00:00
parent 051f804724
commit d23b22aac5
16 changed files with 320 additions and 166 deletions

View File

@@ -11,6 +11,7 @@ import {
import UserType from 'Common/Types/UserType';
import Dictionary from 'Common/Types/Dictionary';
import Port from 'Common/Types/Port';
import https from 'https';
export type RequestHandler = express.RequestHandler;
export type NextFunction = express.NextFunction;
@@ -64,8 +65,24 @@ class Express {
}
public static async launchApplication(
appName: string, port?: Port
appName: string,
port?: Port,
httpsOptions?: {
port?: Port,
sniCallBack?: any
}
): Promise<express.Application> {
const serverOptions = {
SNICallback: httpsOptions?.sniCallBack,
}
if (httpsOptions && httpsOptions.port) {
https.createServer(serverOptions, this.app).listen(httpsOptions?.port.toNumber(), () => {
logger.info(`${appName} HTTPS server started on port: ${httpsOptions?.port?.toNumber()}`);
});
}
return new Promise<express.Application>((resolve: Function) => {
if (!this.app) {
this.setupExpress();
@@ -73,7 +90,7 @@ class Express {
this.app.listen(port?.toNumber() || this.app.get('port'), () => {
// eslint-disable-next-line
logger.info(`${appName} server started on port: ${this.app.get('port')}`);
logger.info(`${appName} server started on port: ${port?.toNumber() || this.app.get('port')}`);
return resolve(this.app);
});
});

View File

@@ -91,8 +91,15 @@ app.use(ExpressUrlEncoded({ limit: '50mb' }));
app.use(logRequest);
const init: Function = async (appName: string, port?: Port): Promise<ExpressApplication> => {
await Express.launchApplication(appName, port);
const init: Function = async (
appName: string,
port?: Port,
httpsOptions?: {
port?: Port,
sniCallBack?: Function
}
): Promise<ExpressApplication> => {
await Express.launchApplication(appName, port, httpsOptions);
LocalCache.setString('app', 'name', appName);
CommonAPI(appName);
@@ -135,19 +142,35 @@ const init: Function = async (appName: string, port?: Port): Promise<ExpressAppl
);
app.post('*', (req: ExpressRequest, res: ExpressResponse) => {
return Response.sendErrorResponse(req, res, new NotFoundException("Not found"))
return Response.sendErrorResponse(
req,
res,
new NotFoundException('Not found')
);
});
app.put('*', (req: ExpressRequest, res: ExpressResponse) => {
return Response.sendErrorResponse(req, res, new NotFoundException("Not found"))
return Response.sendErrorResponse(
req,
res,
new NotFoundException('Not found')
);
});
app.delete('*', (req: ExpressRequest, res: ExpressResponse) => {
return Response.sendErrorResponse(req, res, new NotFoundException("Not found"))
return Response.sendErrorResponse(
req,
res,
new NotFoundException('Not found')
);
});
app.get('*', (req: ExpressRequest, res: ExpressResponse) => {
return Response.sendErrorResponse(req, res, new NotFoundException("Not found"))
return Response.sendErrorResponse(
req,
res,
new NotFoundException('Not found')
);
});
// await OpenTelemetrySDK.start();

View File

@@ -17,9 +17,7 @@ const Logo: FunctionComponent<ComponentProps> = (
onClick={() => {
props.onClick && props.onClick();
}}
imageUrl={Route.fromString(
`${OneUptimeLogo}`
)}
imageUrl={Route.fromString(`${OneUptimeLogo}`)}
/>
</div>
);

View File

@@ -22,9 +22,7 @@ const DashboardUserProfile: FunctionComponent<ComponentProps> = (
<>
<UserProfile
userFullName={UserUtil.getName()}
userProfilePicture={Route.fromString(
`${OneUptimeLogo}`
)}
userProfilePicture={Route.fromString(`${OneUptimeLogo}`)}
>
<UserProfileMenu>
<UserProfileMenuItem

View File

@@ -34,6 +34,21 @@ export default class GreenlockChallenge extends BaseModel {
})
public key?: string = undefined;
@Index()
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.LongText })
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
nullable: false,
unique: false,
})
public token?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],

View File

@@ -305,7 +305,6 @@ export default class StatusPageDomain extends BaseModel {
})
public isAddedtoGreenlock?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadStatusPageDomain],

View File

@@ -26,6 +26,10 @@ upstream status-page-api {
server status-page:3106 weight=10 max_fails=3 fail_timeout=30s;
}
upstream status-page-api-secure {
server status-page:3107 weight=10 max_fails=3 fail_timeout=30s;
}
upstream home {
server home:1444 weight=10 max_fails=3 fail_timeout=30s;
}
@@ -34,13 +38,40 @@ upstream workers {
server workers:3452 weight=10 max_fails=3 fail_timeout=30s;
}
# Acme Verification on port 80
server {
listen 80 default_server;
server_name _; # All domains.
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
# Acme Verification.
location /.well-known {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://status-page-api;
}
}
server {
listen 443 default_server ssl; # Port HTTPS
ssl_certificate /etc/nginx/certs/Cert.crt;
ssl_certificate_key /etc/nginx/certs/Key.key;
listen 80 default_server;
server_name _; # All domains.
proxy_busy_buffers_size 512k;
@@ -86,7 +117,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://status-page-api;
proxy_pass http://status-page-api-secure;
}
# Acme Verification.
@@ -100,7 +131,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://status-page-api;
proxy_pass http://status-page-api-secure;
}
}

View File

@@ -26,6 +26,10 @@ upstream status-page-api {
server status-page:3106 weight=10 max_fails=3 fail_timeout=30s;
}
upstream status-page-api-secure {
server status-page:3107 weight=10 max_fails=3 fail_timeout=30s;
}
upstream home {
server home:1444 weight=10 max_fails=3 fail_timeout=30s;
}
@@ -34,13 +38,40 @@ upstream workers {
server workers:3452 weight=10 max_fails=3 fail_timeout=30s;
}
# Acme Verification on port 80
server {
listen 80 default_server;
server_name _; # All domains.
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
# Acme Verification.
location /.well-known {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://status-page-api;
}
}
server {
listen 443 default_server ssl; # Port HTTPS
ssl_certificate /etc/nginx/certs/Cert.crt;
ssl_certificate_key /etc/nginx/certs/Key.key;
listen 80 default_server;
server_name _; # All domains.
proxy_busy_buffers_size 512k;
@@ -86,7 +117,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://status-page-api;
proxy_pass http://status-page-api-secure;
}
# Acme Verification.
@@ -100,7 +131,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://status-page-api;
proxy_pass http://status-page-api-secure;
}
}

View File

@@ -69,6 +69,8 @@ RUN npm install
EXPOSE 3105
# API
EXPOSE 3106
# HTTPS API
EXPOSE 3107
{{ if eq .Env.ENVIRONMENT "development" }}
#Run the app

View File

@@ -13,6 +13,10 @@ import NotFoundException from 'Common/Types/Exception/NotFoundException';
import BadDataException from 'Common/Types/Exception/BadDataException';
import StatusPageDomain from 'Model/Models/StatusPageDomain';
import StatusPageDomainService from 'CommonServer/Services/StatusPageDomainService';
import Port from 'Common/Types/Port';
import tls from 'tls';
import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService';
import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
export const APP_NAME: string = 'status-page';
@@ -25,7 +29,7 @@ app.get(
const challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: req.params['token'] as string,
token: req.params['token'] as string,
},
select: {
challenge: true,
@@ -54,7 +58,7 @@ app.get(
app.get(
'/status-page-api/cname-verification/:token',
async (req: ExpressRequest, res: ExpressResponse) => {
logger.info("HERE!")
logger.info('HERE!');
const host: string | undefined = req.get('host');
if (!host) {
@@ -90,7 +94,39 @@ app.get(
const init: Function = async (): Promise<void> => {
try {
// init the app
await App(APP_NAME);
await App(APP_NAME, new Port(3106), {
port: new Port(3107),
sniCallback: (serverName: string, callback: Function) => {
logger.info("SNI CALLBACK " + serverName);
GreenlockCertificateService.findOneBy({
query: {
key: serverName,
},
select: {
blob: true,
},
props: {
isRoot: true,
},
}).then((result: GreenlockCertificate | null) => {
if (!result) {
return callback("Certificate not found");
}
const blob = JSON.parse(result.blob as string);
callback(null, new (tls as any).createSecureContext({
cert: blob.cert as string,
key: blob.key as string,
}));
}).catch((err: Error) => {
logger.error(err);
return callback("Server Error. Please try again later.");
});
}
});
// connect to the database.
await PostgresAppInstance.connect(

View File

@@ -24,7 +24,7 @@ app.get('/*', (_req: ExpressRequest, res: ExpressResponse) => {
const init: Function = async (): Promise<void> => {
try {
// init the app
await App(APP_NAME, new Port(3106));
await App(APP_NAME, new Port(3105));
} catch (err) {
logger.error('App Init Failed:');
logger.error(err);

View File

@@ -64,10 +64,10 @@ const greenlock: any = Greenlock.create({
notify: function (event: string, details: any) {
if ('error' === event) {
logger.error("Greenlock Notify: " + event);
logger.error('Greenlock Notify: ' + event);
logger.error(details);
}
logger.info("Greenlock Notify: " + event);
logger.info('Greenlock Notify: ' + event);
logger.info(details);
},
@@ -92,7 +92,7 @@ router.delete(
}
await greenlock.remove({
subject: body['domain']
subject: body['domain'],
});
return Response.sendEmptyResponse(req, res);
@@ -116,7 +116,7 @@ router.post(
await greenlock.add({
subject: body['domain'],
altnames: [body['domain']]
altnames: [body['domain']],
});
return Response.sendEmptyResponse(req, res);
@@ -149,15 +149,6 @@ router.get(
}
);
RunCron(
'StatusPageCerts:Renew',
IsDevelopment ? EVERY_MINUTE : EVERY_HOUR,
async () => {
// fetch all domains wiht expired certs.
await greenlock.renew({});
}
);
RunCron(
'StatusPageCerts:OrderCerts',
IsDevelopment ? EVERY_MINUTE : EVERY_HOUR,
@@ -168,12 +159,12 @@ RunCron(
await StatusPageDomainService.findBy({
query: {
isAddedtoGreenlock: true,
isSslProvisioned: false
isSslProvisioned: false,
},
select: {
_id: true,
greenlockConfig: true,
fullDomain: true
fullDomain: true,
},
limit: LIMIT_MAX,
skip: 0,
@@ -187,9 +178,7 @@ RunCron(
`StatusPageCerts:OrderCerts - Checking CNAME ${domain.fullDomain}`
);
await greenlock.order(domain.greenlockConfig);
}
}
);
@@ -243,7 +232,7 @@ RunCron(
await greenlock.add({
subject: domain.fullDomain,
altnames: [domain.fullDomain]
altnames: [domain.fullDomain],
});
await StatusPageDomainService.updateOneById({
@@ -334,7 +323,6 @@ RunCron(
}
);
RunCron(
'StatusPageCerts:CheckSslProvisioningStatus',
IsDevelopment ? EVERY_MINUTE : EVERY_HOUR,
@@ -394,28 +382,31 @@ RunCron(
}
);
const checkCnameValidation: Function = async (
fulldomain: string,
token: string
): Promise<boolean> => {
logger.info("Check CNAMeValidation.")
logger.info('Check CNAMeValidation.');
try {
const agent = new https.Agent({
rejectUnauthorized: false
rejectUnauthorized: false,
});
const result = await axios.get('https://' + fulldomain + '/status-page-api/cname-verification/' + token, { httpsAgent: agent });
const result = await axios.get(
'https://' +
fulldomain +
'/status-page-api/cname-verification/' +
token,
{ httpsAgent: agent }
);
if (result.status === 200) {
return true;
} else {
return false;
}
return false;
} catch (err) {
logger.error(err);
return false;
return false;
}
};
@@ -423,19 +414,20 @@ const isSslProvisioned: Function = async (
fulldomain: string,
token: string
): Promise<boolean> => {
try {
const result = await axios.get(
'https://' +
fulldomain +
'/status-page-api/cname-verification/' +
token
);
const result = await axios.get('https://' + fulldomain + '/status-page-api/cname-verification/' + token);
if (result.status === 200) {
return true;
} else {
return false;
}
return false;
} catch (err) {
logger.error(err);
return false;
return false;
}
};

View File

@@ -4,103 +4,110 @@ import logger from 'CommonServer/Utils/Logger';
// because greenlock package expects module.exports.
module.exports = {
init: async (): Promise<null> => {
logger.info("Greenlock HTTP Challenge Init");
return Promise.resolve(null);
},
set: async (data: any): Promise<null> => {
logger.info("Greenlock HTTP Challenge Set");
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
let challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
challenge = new GreenlockChallenge();
challenge.key = key;
challenge.challenge = ch.keyAuthorization;
await GreenlockChallengeService.create({
data: challenge,
props: {
isRoot: true,
},
});
} else {
challenge.challenge = ch.keyAuthorization;
await GreenlockChallengeService.updateOneById({
id: challenge.id!,
data: challenge,
props: {
isRoot: true,
},
});
}
//
return null;
},
get: async (data: any): Promise<null | any> => {
logger.info("Greenlock HTTP Challenge Get");
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
const challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
return null;
}
return { keyAuthorization: challenge.challenge };
},
remove: async (data: any): Promise<null> => {
logger.info("Greenlock HTTP Challenge Remove");
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
await GreenlockChallengeService.deleteOneBy({
query: {
key: key,
create: (_opts: any) => {
return {
init: async (): Promise<null> => {
logger.info('Greenlock HTTP Challenge Init');
return Promise.resolve(null);
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
return null;
},
set: async (data: any): Promise<null> => {
logger.info('Greenlock HTTP Challenge Set');
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
const token: string = ch.token;
let challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
challenge = new GreenlockChallenge();
challenge.key = key;
challenge.token = token;
challenge.challenge = ch.keyAuthorization;
await GreenlockChallengeService.create({
data: challenge,
props: {
isRoot: true,
},
});
} else {
challenge.challenge = ch.keyAuthorization;
challenge.token = token;
await GreenlockChallengeService.updateOneById({
id: challenge.id!,
data: challenge,
props: {
isRoot: true,
},
});
}
//
return null;
},
get: async (data: any): Promise<null | any> => {
logger.info('Greenlock HTTP Challenge Get');
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
const challenge: GreenlockChallenge | null =
await GreenlockChallengeService.findOneBy({
query: {
key: key,
},
select: {
_id: true,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
if (!challenge) {
return null;
}
return { keyAuthorization: challenge.challenge };
},
remove: async (data: any): Promise<null> => {
logger.info('Greenlock HTTP Challenge Remove');
logger.info(data);
const ch: any = data.challenge;
const key: string = ch.identifier.value + '#' + ch.token;
await GreenlockChallengeService.deleteOneBy({
query: {
key: key,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
return null;
},
}
}
};

View File

@@ -19,7 +19,7 @@ module.exports = {
// Optional (wildcard support): find a certificate with `wildname` as an altname
// { subject, altnames, renewAt, deletedAt, challenges, ... }
logger.info("Greenlock Manager Get");
logger.info('Greenlock Manager Get');
logger.info(servername);
const domain: StatusPageDomain | null =
await StatusPageDomainService.findOneBy({
@@ -37,11 +37,15 @@ module.exports = {
});
if (!domain || !domain.greenlockConfig) {
logger.info("Greenlock Manager GET " + servername+" - No domain found.");
logger.info(
'Greenlock Manager GET ' +
servername +
' - No domain found.'
);
return undefined;
}
logger.info("Greenlock Manager GET " + servername + " RESULT");
logger.info('Greenlock Manager GET ' + servername + ' RESULT');
logger.info(domain.greenlockConfig);
return domain.greenlockConfig;
@@ -49,7 +53,7 @@ module.exports = {
// Set
set: async (opts: any) => {
logger.info("Greenlock Manager Set");
logger.info('Greenlock Manager Set');
logger.info(opts);
// { subject, altnames, renewAt, deletedAt }

View File

@@ -4,7 +4,7 @@ import GreenlockCertificate from 'Model/Models/GreenlockCertificate';
import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService';
module.exports = {
create: () => {
create: (_opts: any) => {
const saveCertificate: Function = async (
id: string,
blob: string
@@ -116,7 +116,7 @@ module.exports = {
},
},
certificate: {
certificates: {
setKeypair: async (opts: any): Promise<null> => {
// The ID is a string that doesn't clash between accounts and certificates.
// That's all you need to know... unless you're doing something special (in which case you're on your own).
@@ -176,7 +176,7 @@ module.exports = {
// but it's easiest to implement last since it's not useful until there
// are certs that can actually be loaded from storage.
check: async (opts: any): Promise<null | any> => {
const id: string = opts.certificate.id || opts.subject;
const id: string = opts.certificate?.id || opts.subject;
const certblob: any = await getCertificate(id);
if (!certblob) {

View File

@@ -139,8 +139,9 @@ services:
status-page:
ports:
- '3105:3105'
- '3106:3106'
- '3105:3105' # UI Port
- '3106:3106' # HTTP API Port
- '3107:3107' # HTTPS API Port
{{ if eq .Env.ENVIRONMENT "development" }}
- 9764:9229 # Debugging port.
{{ end }}