backend-app-api: refactor hostFactory into createHttpServer
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
+19
-83
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user