refactor: update dependencies

This commit is contained in:
Michel Jonathan Schmitz
2020-11-03 12:52:54 +01:00
committed by Rainer Killinger
parent 9512bca329
commit 053a6ce23f
9 changed files with 3694 additions and 1689 deletions

View File

@@ -13,68 +13,57 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Logger} from '@openstapps/logger';
import {execSync} from 'child_process';
import * as config from 'config';
import * as Dockerode from 'dockerode';
import {readFile, writeFileSync} from 'fs-extra';
import {render} from 'mustache';
import {ConfigFile, logger} from './common';
import {asyncReadFile, asyncWriteFile, configFile} from './common';
import {getContainers, getTemplateView} from './main';
// handle unhandled promise rejections
process.on('unhandledRejection', (error: Error) => {
logger.error(error.message);
logger.info(error.stack);
process.on('unhandledRejection', async (error) => {
await Logger.error(error);
process.exit(1);
});
let containerHashCache = '';
const configFile: ConfigFile = config.util.toObject();
/**
* Reads the container information from the docker socket and updates
* the nginx config if necessary
*
* The function will call itself again every 10s
* Reads the container information from the docker socket and updates the nginx config if necessary
*/
async function updateNginxConfig() {
const containers = await getContainers();
const containerHash = containers.map((container: Dockerode.ContainerInfo) => {
return container.Id;
}).join(',');
const containerHash = containers
.map((container: Dockerode.ContainerInfo) => {
return container.Id;
})
.join(',');
// if containers changed -> write config file, reload nginx
if (containerHash !== containerHashCache) {
logger.log('docker container changed');
logger.log('Generating new NGINX configuration');
Logger.log('Generating new NGINX configuration');
// render nginx config file
const nginxConfig = render(await readFile('nginx.conf.template', 'utf8'), await getTemplateView(containers));
const nginxConfig = render(await asyncReadFile('nginx.conf.template', 'utf8'), await getTemplateView(containers));
logger.log(`containers (${containerHash}) matched the configuration.`);
Logger.log(`containers (${containerHash}) matched the configuration.`);
containerHashCache = containerHash;
logger.log(`Writing new config file "${configFile.output}"`);
// overwrite nginx config file with our rendered one
writeFileSync(configFile.output, nginxConfig, 'utf8');
Logger.log(`Writing new config file "${configFile.output}"`);
// overwrite nginx config file with our rendered one
await asyncWriteFile(configFile.output, nginxConfig, 'utf8');
Logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file');
logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file');
execSync('nginx -s reload');
}
// tslint:disable-next-line:no-magic-numbers - set timeout to update configuration again in 30s
setTimeout(updateNginxConfig, 30000);
}
function forever() {
// start the process of dynamic nginx configuration
updateNginxConfig().then(() => {
// check for changes again in 10 seconds
setTimeout(forever, 10000);
}).catch((err) => {
throw err;
});
}
// start the process that checks the docker socket periodically
forever();
// tslint:disable-next-line:no-floating-promises - start the process that checks the docker socket periodically
updateNginxConfig();

View File

@@ -14,48 +14,99 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Logger} from '@openstapps/logger';
import {SMTP} from '@openstapps/logger/lib/SMTP';
import {SMTP} from '@openstapps/logger/lib/smtp';
import config from 'config';
import {existsSync, readFile, writeFile} from 'fs';
import {promisify} from 'util';
// use SMTP as a default monitoring system for logger.error();
export const logger = new Logger(SMTP.getInstance());
// set transport on logger
Logger.setTransport(SMTP.getInstance());
export const asyncReadFile = promisify(readFile);
export const asyncWriteFile = promisify(writeFile);
/**
* A representation of the file paths of the needed ssl certificates
*/
export interface SSLFilePaths {
certificate: string;
certificateChain: string;
certificateKey: string;
dhparam: string;
/**
* Path to SSL certificate
*/
certificate: string;
/**
* Path to SSL certificate chain
*/
certificateChain: string;
/**
* Path to SSL certificate key
*/
certificateKey: string;
/**
* Path to SSL DHParam
*/
dhparam: string;
}
/**
* A representation of the config file
*/
export interface ConfigFile {
activeVersions: string[];
hiddenRoutes: string[];
outdatedVersions: string[];
output: string;
sslFilePaths: SSLFilePaths;
visibleRoutes: string[];
/**
* List of active version
*/
activeVersions: string[];
/**
* List of hidden routes
*/
hiddenRoutes: string[];
/**
* List of outdated versions
*/
outdatedVersions: string[];
/**
* Output?! TODO
*/
output: string;
/**
* SSL file paths
*/
sslFilePaths: SSLFilePaths;
/**
* List of visible routes
*/
visibleRoutes: string[];
}
/**
* A view object to render the nginx config template
*/
export interface TemplateView {
dockerVersionMap: string;
hiddenRoutes: string;
listener: string;
staticRoute: string;
visibleRoutes: string;
/**
* Docker version map
*/
dockerVersionMap: string;
/**
* Hidden routes
*/
hiddenRoutes: string;
/**
* Listener
*/
listener: string;
/**
* Static route
*/
staticRoute: string;
/**
* Visible routes
*/
visibleRoutes: string;
}
/**
* Nginx protocol parameters to harden serverside settings
*/
export const protocolHardeningParameters: string = `
export const protocolHardeningParameters = `
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains;";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
@@ -65,7 +116,7 @@ add_header X-XSS-Protection "1; mode=block";`;
/**
* Nginx ssl parameters to harden serverside settings
*/
export const sslHardeningParameters: string = `
export const sslHardeningParameters = `
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;
@@ -75,3 +126,17 @@ ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;`;
/**
* Config file
*/
export const configFile: ConfigFile = config.util.toObject();
/**
* Check if path is a specific file type
*/
export function isFileType(path: string, fileType: string) {
const regExp = new RegExp(`.*\.${fileType}$`);
return existsSync(path) && regExp.test(path);
}

View File

@@ -13,111 +13,117 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as config from 'config';
import * as Dockerode from 'dockerode';
import {existsSync, readFile} from 'fs-extra';
import {Logger} from '@openstapps/logger';
import Dockerode from 'dockerode';
import {render} from 'mustache';
import {join} from 'path';
import {
ConfigFile,
logger,
asyncReadFile,
configFile,
isFileType,
protocolHardeningParameters,
SSLFilePaths,
sslHardeningParameters,
TemplateView,
} from './common';
const configFile: ConfigFile = config.util.toObject();
/**
* Checks if a ContainerInfo matches a name and version regex
* @param {string} name
* @param {RegExp} versionRegex
* @param {Dockerode.ContainerInfo} container
* @return {boolean}
*
* @param name Name to check
* @param versionRegex Version regex to check
* @param container Container info for check
*/
export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): boolean {
return typeof container.Labels['stapps.version'] === 'string' &&
container.Labels['stapps.version'].match(versionRegex) !== null &&
typeof container.Labels['com.docker.compose.service'] === 'string' &&
container.Labels['com.docker.compose.service'] === name;
return typeof container.Labels['stapps.version'] === 'string'
&& container.Labels['stapps.version'].match(versionRegex) !== null
&& typeof container.Labels['com.docker.compose.service'] === 'string'
&& container.Labels['com.docker.compose.service'] === name;
}
/**
* Gets Gateway (ip:port) of given ContainerInfo. Returns an empty String if there is no Gateway.
* Gets Gateway (ip:port) of given ContainerInfo
*
* Returns an empty String if there is no Gateway.
* This assumes that a backend runs in the container and it exposes one port.
* @param {Dockerode.ContainerInfo} container
* @return {string}
*
* @param container Container info of which to get address from
*/
export function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): string {
export async function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): Promise<string> {
if (container.Ports.length === 0) {
logger.error(
'Container',
container.Id,
'does not advertise any Port. Please expose a Port if the container should be accessible by NGINX.',
);
await Logger.error(`Container ${container.Id} does not advertise any port.
Please expose a port if the container should be accessible by NGINX.`);
return '';
} else {
// ip:port
return container.Ports[0].IP + ':' + container.Ports[0].PublicPort;
}
// ip:port
return `${container.Ports[0].IP}:${container.Ports[0].PublicPort}`;
}
/**
* Generates an upstream map. It maps all stapps-backend-containers to an gateway
* @param {string[]} activeVersions
* @param {string[]} outdatedVersions
* @param {Dockerode.ContainerInfo[]} containers
* @return {string}
* Generates an upstream map
*
* It maps all stapps-backend-containers to an gateway.
*
* @param activeVersions List of active versions
* @param outdatedVersions List of outdated versions
* @param containers List of container infos
*/
export function generateUpstreamMap(
export async function generateUpstreamMap(
activeVersions: string[],
outdatedVersions: string[],
containers: Dockerode.ContainerInfo[],
): string {
): Promise<string> {
let result = 'map $http_x_stapps_version $proxyurl {\n default unsupported;\n';
let upstreams = '';
let foundMatchingContainer = false;
// active versions
result += activeVersions.map((activeVersionRegex) => {
const upstreamName = activeVersionRegex.replace(/[\\|\.|\+]/g, '_');
result += (await Promise.all(
activeVersions
.map(async (activeVersionRegex) => {
const upstreamName = activeVersionRegex.replace(/[\\|.+]/g, '_');
const activeBackends = containers.filter((container) => {
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
});
const activeBackends = containers.filter((container) => {
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
});
if (activeBackends.length > 0) {
if (activeBackends.length > 0) {
foundMatchingContainer = true;
foundMatchingContainer = true;
if (activeBackends.length > 1) {
throw new Error('Multiple backends for one version found.');
}
if (activeBackends.length > 1) {
throw new Error('Multiple backends for one version found.');
}
const gateWayOfContainer = await getGatewayOfStAppsBackend(activeBackends[0]);
const gateWayOfContainer = getGatewayOfStAppsBackend(activeBackends[0]);
if (gateWayOfContainer.length !== 0) {
upstreams += `\nupstream ${upstreamName} {\n server ${gateWayOfContainer};\n}`;
return ` \"~${activeVersionRegex}\" ${upstreamName};\n`;
}
return ` \"~${activeVersionRegex}\" unavailable;\n`;
}
await Logger.error('No backend for version', activeVersionRegex, 'found');
if (gateWayOfContainer.length !== 0) {
upstreams += `\nupstream ${upstreamName} {\n server ${gateWayOfContainer};\n}`;
return ` \"~${activeVersionRegex}\" ${upstreamName};\n`;
} else {
return ` \"~${activeVersionRegex}\" unavailable;\n`;
}
} else {
logger.error('No backend for version', activeVersionRegex, 'found');
return ` \"~${activeVersionRegex}\" unavailable;\n`;
}
}).join('');
}),
)).join('');
// outdated versions
result += outdatedVersions.map((outdatedVersionRegex) => {
return ` \"~${outdatedVersionRegex}\" outdated;`;
}).join('') + '\n\}';
result += outdatedVersions
.map((outdatedVersionRegex) => {
return ` \"~${outdatedVersionRegex}\" outdated;`;
})
.join('');
result += '\n\}';
if (!foundMatchingContainer) {
logger.error(
await Logger.error(
'No container with matching version label found. Please start a container with a matching version Label.',
);
}
@@ -127,74 +133,65 @@ export function generateUpstreamMap(
/**
* Generates http or https listener
* @param sslFiles
* @returns {string}
*/
function generateListener(sslFilePaths: SSLFilePaths) {
function isSSLCert(path: string) {
return existsSync(path) && /.*\.crt$/.test(path);
}
function isSSLKey(path: string) {
return existsSync(path) && /.*\.key$/.test(path);
}
function isPEMFile(path: string) {
return existsSync(path) && /.*\.pem$/.test(path);
}
export function generateListener(sslFilePaths: SSLFilePaths) {
let listener = '';
if (typeof sslFilePaths !== 'undefined' &&
typeof sslFilePaths.certificate === 'string' && isSSLCert(sslFilePaths.certificate) &&
typeof sslFilePaths.certificateChain === 'string' && isSSLCert(sslFilePaths.certificate) &&
typeof sslFilePaths.certificateKey === 'string' && isSSLKey(sslFilePaths.certificate) &&
typeof sslFilePaths.dhparam === 'string' && isPEMFile(sslFilePaths.dhparam)
) {
typeof sslFilePaths.certificate !== 'undefined' && isFileType(sslFilePaths.certificate,'crt') &&
typeof sslFilePaths.certificateChain !== 'undefined' && isFileType(sslFilePaths.certificateChain,'crt') &&
typeof sslFilePaths.certificateKey !== 'undefined' && isFileType(sslFilePaths.certificateKey,'key') &&
typeof sslFilePaths.dhparam !== 'undefined' && isFileType(sslFilePaths.dhparam,'pem')
) {
// https listener
listener = 'listen 443 ssl default_server;\n' +
`ssl_certificate ${sslFilePaths.certificate};\n` +
`ssl_certificate_key ${sslFilePaths.certificateKey};\n` +
`ssl_trusted_certificate ${sslFilePaths.certificateChain};\n` +
`ssl_dhparam ${sslFilePaths.dhparam};\n` +
`${sslHardeningParameters}`;
listener = `listen 443 ssl default_server;
ssl_certificate ${sslFilePaths.certificate};
ssl_certificate_key ${sslFilePaths.certificateKey};
ssl_trusted_certificate ${sslFilePaths.certificateChain};
ssl_dhparam ${sslFilePaths.dhparam};
${sslHardeningParameters}`;
} else {
// default http listener
listener = 'listen 80 default_server;';
logger.warn('Https usage is not setup properly, falling back to http!');
Logger.warn('Https usage is not setup properly, falling back to http!');
}
listener = `${listener}\n${protocolHardeningParameters}\n`;
listener = `${listener}
${protocolHardeningParameters}
`;
return listener;
}
/**
* Render a mustache template file with given view object
* @param path (path to file)
* @param view
* @param callback
*
* @param path Path to template file
* @param view Data to render template with
*/
async function renderTemplate(path: string, view: any): Promise<string> {
const content = await readFile(path, 'utf8');
async function renderTemplate(path: string, view: unknown): Promise<string> {
const content = await asyncReadFile(path, 'utf8');
return render(content, view);
}
/**
* Returns view for nginx config file
* @param containers
*
* @param containers List of container info
*/
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
const cors = await asyncReadFile('./fixtures/cors.template', 'utf8');
const cors = await readFile('./fixtures/cors.template', 'utf8');
const visibleRoutesPromises = configFile.visibleRoutes.map((route) => {
const visibleRoutesPromises = configFile.visibleRoutes.map(async (route) => {
return renderTemplate(join('fixtures', 'visibleRoute.template'), {
cors,
route,
});
});
const hiddenRoutesPromises = configFile.hiddenRoutes.map((route) => {
const hiddenRoutesPromises = configFile.hiddenRoutes.map(async (route) => {
return renderTemplate(join('fixtures', 'hiddenRoute.template'), {
cors,
route,
@@ -202,7 +199,7 @@ export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Pr
});
return {
dockerVersionMap: generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
dockerVersionMap: await generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''),
listener: generateListener(configFile.sslFilePaths),
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}),
@@ -212,7 +209,8 @@ export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Pr
/**
* Read the list of docker containers
* @param pathToDockerSocket
*
* @param pathToDockerSocket Path to docker socket
*/
export async function getContainers(pathToDockerSocket = '/var/run/docker.sock'): Promise<Dockerode.ContainerInfo[]> {
const docker = new Dockerode({
@@ -221,12 +219,15 @@ export async function getContainers(pathToDockerSocket = '/var/run/docker.sock')
const containers = await docker.listContainers();
/* istanbul ignore next */
if (containers.length === 0) {
throw new Error(
'No running docker containers found.' +
`Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
`No running docker containers found.
Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
);
}
/* istanbul ignore next */
return containers;
}