Merge pull request #1996 from dfds/dfds-servicebuilder-https-enabled-v2
Dfds servicebuilder https enabled v2
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user