backend-app-api: refactor hostFactory into createHttpServer

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-01-04 17:52:38 +01:00
parent 64f40724d5
commit 950e0392aa
5 changed files with 137 additions and 84 deletions
+3 -1
View File
@@ -48,12 +48,14 @@
"minimatch": "^5.0.0",
"node-forge": "^1.3.1",
"selfsigned": "^2.0.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/fs-extra": "^9.0.3",
"@types/node-forge": "^1.3.0"
"@types/node-forge": "^1.3.0",
"@types/stoppable": "^1.1.0"
},
"files": [
"dist",
@@ -0,0 +1,98 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as http from 'http';
import * as https from 'https';
import stoppableServer from 'stoppable';
import { RequestListener } from 'http';
import { LoggerService } from '@backstage/backend-plugin-api';
import { HttpServerOptions, ExtendedHttpServer } from './types';
import { getGeneratedCertificate } from './getGeneratedCertificate';
/**
* Creates a Node.js HTTP or HTTPS server instance.
*
* @public
*/
export async function createHttpServer(
listener: RequestListener,
options: HttpServerOptions,
deps: { logger: LoggerService },
): Promise<ExtendedHttpServer> {
const server = await createServer(listener, options, deps);
const stopper = stoppableServer(server, 0);
return Object.assign(server, {
start() {
return new Promise<void>((resolve, reject) => {
const handleStartupError = (error: Error) => {
server.close();
reject(error);
};
server.on('error', handleStartupError);
const { host, port } = options.listen;
server.listen(port, host, () => {
server.off('error', handleStartupError);
deps.logger.info(`Listening on ${host}:${port}`);
resolve();
});
});
},
stop() {
return new Promise<void>((resolve, reject) => {
stopper.stop((error?: Error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
},
port() {
const address = server.address();
if (typeof address === 'string' || address === null) {
throw new Error(`Unexpected server address '${address}'`);
}
return address.port;
},
});
}
async function createServer(
listener: RequestListener,
options: HttpServerOptions,
deps: { logger: LoggerService },
): Promise<http.Server> {
if (options.https) {
const { certificate } = options.https;
if (certificate.type === 'generated') {
const credentials = await getGeneratedCertificate(
certificate.hostname,
deps.logger,
);
return https.createServer(credentials, listener);
}
return https.createServer(certificate, listener);
}
return http.createServer(listener);
}
@@ -16,86 +16,16 @@
import fs from 'fs-extra';
import { resolve as resolvePath, dirname } from 'path';
import express from 'express';
import * as http from 'http';
import * as https from 'https';
import { LoggerService } from '@backstage/backend-plugin-api';
import { HttpsSettings } from './config';
import forge from 'node-forge';
const FIVE_DAYS_IN_MS = 5 * 24 * 60 * 60 * 1000;
const IP_HOSTNAME_REGEX = /:|^\d+\.\d+\.\d+\.\d+$/;
/**
* Creates a Http server instance based on an Express application.
*
* @param app - The Express application object
* @param logger - Optional Winston logger object
* @returns A Http server instance
*
*/
export function createHttpServer(
app: express.Express,
logger?: LoggerService,
): http.Server {
logger?.info('Initializing http server');
return http.createServer(app);
}
/**
* Creates a Https server instance based on an Express application.
*
* @param app - The Express application object
* @param httpsSettings - HttpsSettings for self-signed certificate generation
* @param logger - Optional Winston logger object
* @returns A Https server instance
*
*/
export async function createHttpsServer(
app: express.Express,
httpsSettings: HttpsSettings,
logger?: LoggerService,
): Promise<http.Server> {
logger?.info('Initializing https server');
let credentials: { key: string | Buffer; cert: string | Buffer };
if ('hostname' in httpsSettings?.certificate) {
credentials = await getGeneratedCertificate(
httpsSettings.certificate.hostname,
logger,
);
} else {
logger?.info('Loading certificate from config');
credentials = {
key: httpsSettings?.certificate?.key,
cert: httpsSettings?.certificate?.cert,
};
}
if (!credentials.key || !credentials.cert) {
throw new Error('Invalid HTTPS credentials');
}
return https.createServer(credentials, app) as http.Server;
}
function getCertificateExpiration(cert: string, logger?: LoggerService) {
try {
const crt = forge.pki.certificateFromPem(cert);
return crt.validity.notAfter.getTime() - Date.now();
} catch (error) {
logger?.warn(`Unable to parse self-signed certificate. ${error}`);
return 0;
}
}
async function getGeneratedCertificate(
export async function getGeneratedCertificate(
hostname: string,
logger?: LoggerService,
logger: LoggerService,
) {
const hasModules = await fs.pathExists('node_modules');
let certPath;
@@ -109,24 +39,30 @@ async function getGeneratedCertificate(
}
if (await fs.pathExists(certPath)) {
const cert = await fs.readFile(certPath);
const remainingMs = getCertificateExpiration(cert.toString(), logger);
if (remainingMs > FIVE_DAYS_IN_MS) {
logger?.info('Using existing self-signed certificate');
return {
key: cert,
cert,
};
try {
const cert = await fs.readFile(certPath);
const crt = forge.pki.certificateFromPem(cert.toString());
const remainingMs = crt.validity.notAfter.getTime() - Date.now();
if (remainingMs > FIVE_DAYS_IN_MS) {
logger.info('Using existing self-signed certificate');
return {
key: cert,
cert,
};
}
} catch (error) {
logger.warn(`Unable to use existing self-signed certificate, ${error}`);
}
}
logger?.info('Generating new self-signed certificate');
const newCert = await createCertificate(hostname);
logger.info('Generating new self-signed certificate');
const newCert = await generateCertificate(hostname);
await fs.writeFile(certPath, newCert.cert + newCert.key, 'utf8');
return newCert;
}
async function createCertificate(hostname: string) {
async function generateCertificate(hostname: string) {
const attributes = [
{
name: 'commonName',
@@ -14,6 +14,21 @@
* limitations under the License.
*/
import * as http from 'http';
/**
* An HTTP server extended with utility methods.
*
* @public
*/
export interface ExtendedHttpServer extends http.Server {
start(): Promise<void>;
stop(): Promise<void>;
port(): number;
}
/**
* Options for starting up an HTTP server.
*
+2
View File
@@ -3388,6 +3388,7 @@ __metadata:
"@types/express": ^4.17.6
"@types/fs-extra": ^9.0.3
"@types/node-forge": ^1.3.0
"@types/stoppable": ^1.1.0
cors: ^2.8.5
express: ^4.17.1
express-promise-router: ^4.1.0
@@ -3395,6 +3396,7 @@ __metadata:
minimatch: ^5.0.0
node-forge: ^1.3.1
selfsigned: ^2.0.0
stoppable: ^1.1.0
winston: ^3.2.1
languageName: unknown
linkType: soft