Merge pull request #1996 from dfds/dfds-servicebuilder-https-enabled-v2

Dfds servicebuilder https enabled v2
This commit is contained in:
Fredrik Adelöw
2020-08-21 14:04:25 +02:00
committed by GitHub
6 changed files with 221 additions and 10 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ const Background = props => {
left: 0,
right: 0,
bottom: 0,
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg);`,
backgroundImage: `linear-gradient( to bottom, rgb(18, 18, 18), rgba(0, 0, 0, 0) ), url(../img/dot.svg)`,
}}
/>
</div>
+2 -1
View File
@@ -43,7 +43,8 @@
"lodash": "^4.17.15",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
"winston": "^3.2.1"
"winston": "^3.2.1",
"selfsigned": "^1.10.7"
},
"peerDependencies": {
"pg-connection-string": "^2.3.0"
@@ -19,7 +19,7 @@ import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
import helmet from 'helmet';
import { Server } from 'http';
import * as http from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
import { useHotCleanup } from '../../hot';
@@ -30,7 +30,13 @@ import {
requestLoggingHandler,
} from '../../middleware';
import { ServiceBuilder } from '../types';
import { readBaseOptions, readCorsOptions } from './config';
import {
readBaseOptions,
readCorsOptions,
readHttpsSettings,
HttpsSettings,
} from './config';
import { createHttpServer, createHttpsServer } from './hostFactory';
const DEFAULT_PORT = 7000;
// '' is express default, which listens to all interfaces
@@ -41,6 +47,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private httpsSettings: HttpsSettings | undefined;
private routers: [string, Router][];
// Reference to the module where builder is created - needed for hot module
// reloading
@@ -70,6 +77,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
this.corsOptions = corsOptions;
}
const httpsSettings = readHttpsSettings(backendConfig);
if (httpsSettings) {
this.httpsSettings = httpsSettings;
}
return this;
}
@@ -88,6 +100,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
setHttpsSettings(settings: HttpsSettings): ServiceBuilder {
this.httpsSettings = settings;
return this;
}
enableCors(options: cors.CorsOptions): ServiceBuilder {
this.corsOptions = options;
return this;
@@ -98,9 +115,15 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
start(): Promise<Server> {
start(): Promise<http.Server> {
const app = express();
const { port, host, logger, corsOptions } = this.getOptions();
const {
port,
host,
logger,
corsOptions,
httpsSettings,
} = this.getOptions();
app.use(helmet());
if (corsOptions) {
@@ -121,20 +144,24 @@ export class ServiceBuilderImpl implements ServiceBuilder {
reject(e);
});
const server = stoppable(
app.listen(port, host, () => {
const server: http.Server = httpsSettings
? createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
const stoppableServer = stoppable(
server.listen(port, host, () => {
logger.info(`Listening on ${host}:${port}`);
}),
0,
);
useHotCleanup(this.module, () =>
server.stop((e: any) => {
stoppableServer.stop((e: any) => {
if (e) console.error(e);
}),
);
resolve(server);
resolve(stoppableServer);
});
}
@@ -143,12 +170,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
httpsSettings?: HttpsSettings;
} {
return {
port: this.port ?? DEFAULT_PORT,
host: this.host ?? DEFAULT_HOST,
logger: this.logger ?? getRootLogger(),
corsOptions: this.corsOptions,
httpsSettings: this.httpsSettings,
};
}
}
@@ -22,6 +22,41 @@ export type BaseOptions = {
listenHost?: string;
};
export type CertificateOptions = {
key?: CertificateKeyOptions;
attributes?: CertificateAttributeOptions;
};
export type CertificateKeyOptions = {
size?: number;
algorithm?: string;
days?: number;
};
export type CertificateAttributeOptions = {
commonName?: string;
};
export type HttpsSettings = {
certificate: CertificateSigningOptions | CertificateReferenceOptions;
};
export type CertificateReferenceOptions = {
key: string;
cert: string;
};
export type CertificateSigningOptions = {
algorithm: string;
size?: number;
days?: number;
attributes?: CertificateAttributes;
};
export type CertificateAttributes = {
commonName?: string;
};
/**
* Reads some base options out of a config object.
*
@@ -40,6 +75,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
if (typeof config.get('listen') === 'string') {
// TODO(freben): Expand this to support more addresses and perhaps optional
const { host, port } = parseListenAddress(config.getString('listen'));
return removeUnknown({
listenPort: port,
listenHost: host,
@@ -49,6 +85,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
return removeUnknown({
listenPort: config.getOptionalNumber('listen.port'),
listenHost: config.getOptionalString('listen.host'),
baseUrl: config.getOptionalString('baseUrl'),
});
}
@@ -86,6 +123,39 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
});
}
/**
* Attempts to read a https settings object from the root of a config object.
*
* @param config The root of a backend config object
* @returns A https settings object, or undefined if not specified
*
* @example
* ```json
* {
* https: {
* certificate: ...
* }
* }
* ```
*/
export function readHttpsSettings(
config: ConfigReader,
): HttpsSettings | undefined {
const cc = config.getOptionalConfig('https');
if (!cc) {
return undefined;
}
const certificateConfig = cc.get('certificate');
const cfg = {
certificate: certificateConfig,
};
return removeUnknown(cfg as HttpsSettings);
}
function getOptionalStringOrStrings(
config: ConfigReader,
key: string,
@@ -0,0 +1,92 @@
/*
* Copyright 2020 Spotify AB
*
* 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 express from 'express';
import * as http from 'http';
import * as https from 'https';
import { Logger } from 'winston';
import { HttpsSettings } from './config';
/**
* 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?: Logger,
): 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 function createHttpsServer(
app: express.Express,
httpsSettings: HttpsSettings,
logger?: Logger,
): http.Server {
logger?.info('Initializing https server');
const credentials: { key: string; cert: string } = {
key: '',
cert: '',
};
const signingOptions: any = httpsSettings?.certificate;
if (signingOptions?.algorithm !== undefined) {
logger?.info('Generating self-signed certificate with attributes');
const certificateAttributes: Array<any> = Object.entries(
signingOptions.attributes,
).map(([name, value]) => ({ name, value }));
// TODO: Create a type def for selfsigned.
const signatures = require('selfsigned').generate(certificateAttributes, {
algorithm: signingOptions?.algorithm,
keySize: signingOptions?.size || 2048,
days: signingOptions?.days || 30,
});
logger?.info('Bootstrapping self-signed certificate');
credentials.key = signatures.private;
credentials.cert = signatures.cert;
} else {
logger?.info('Bootstrapping cert from config');
credentials.key = signingOptions?.key;
credentials.cert = signingOptions?.cert;
}
if (credentials.key === '' || credentials.cert === '') {
throw new Error('Invalid credentials');
}
return https.createServer(credentials, app) as http.Server;
}
@@ -19,6 +19,7 @@ import cors from 'cors';
import { Router, RequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
import { HttpsSettings } from './lib/config';
export type ServiceBuilder = {
/**
@@ -39,6 +40,15 @@ export type ServiceBuilder = {
*/
setPort(port: number): ServiceBuilder;
/**
* Sets the host to listen on.
*
* '' is express default, which listens to all interfaces.
*
* @param host The host to listen on
*/
setHost(host: string): ServiceBuilder;
/**
* Sets the logger to use for service-specific logging.
*
@@ -58,6 +68,15 @@ export type ServiceBuilder = {
*/
enableCors(options: cors.CorsOptions): ServiceBuilder;
/**
* Configure self-signed certificate generation options.
*
* If this method is not called, the resulting service will use sensible defaults
*
* @param options Standard certificate options
*/
setHttpsSettings(settings: HttpsSettings): ServiceBuilder;
/**
* Adds a router (similar to the express .use call) to the service.
*