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

4331
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,88 @@
{ {
"name": "@openstapps/proxy", "name": "@openstapps/proxy",
"version": "0.0.1", "version": "0.0.1",
"description": "Nginx proxy that is dynamically configured by a Node.js script", "description": "NGINX proxy that is dynamically configured by a Node.js script",
"main": "./lib/cli.js", "main": "./lib/cli.js",
"dependencies": { "dependencies": {
"@openstapps/logger": "0.0.5", "@openstapps/logger": "0.5.0",
"@types/config": "0.0.34", "@types/config": "0.0.36",
"@types/dockerode": "2.5.12", "@types/dockerode": "2.5.34",
"@types/sha1": "1.1.1", "@types/node": "10.17.17",
"config": "3.0.1", "@types/sha1": "1.1.2",
"fs-extra": "7.0.1", "config": "3.3.2",
"mustache": "3.0.1", "dockerode": "3.2.1",
"sha1": "1.1.1", "mustache": "4.0.1"
"typescript": "3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/configuration": "0.6.0", "@openstapps/configuration": "0.25.0",
"@types/chai": "4.1.7", "@testdeck/mocha": "0.1.2",
"@types/mustache": "0.8.32", "@types/chai": "4.2.12",
"@types/sinon": "7.0.6", "@types/chai-spies": "1.0.2",
"@types/mustache": "4.0.1",
"chai": "4.2.0", "chai": "4.2.0",
"conventional-changelog-cli": "2.0.12", "chai-spies": "1.0.0",
"dockerode": "2.5.8", "conventional-changelog-cli": "2.1.0",
"mocha": "5.2.0", "mocha": "7.1.1",
"mocha-typescript": "1.1.17", "nyc": "15.1.0",
"prepend-file-cli": "1.0.6", "prepend-file-cli": "1.0.6",
"rimraf": "2.6.3", "rimraf": "3.0.2",
"sinon": "7.2.4", "ts-node": "8.6.2",
"ts-node": "8.0.2", "tslint": "6.1.3",
"tslint": "5.12.1", "typedoc": "0.18.0",
"typedoc": "0.14.2" "typescript": "4.0.3"
}, },
"scripts": { "scripts": {
"build": "npm run tslint && npm run compile && npm run documentation", "build": "npm run tslint && npm run compile",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'",
"check-configuration": "openstapps-configuration", "check-configuration": "openstapps-configuration",
"compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'", "compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'",
"documentation": "typedoc --includeDeclarations --excludeExternals --mode modules --out docs src", "documentation": "typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src",
"prepublishOnly": "npm run build", "postversion": "npm run changelog",
"tslint": "tslint 'src/**/*.ts'", "prepublishOnly": "npm ci && npm run build",
"test": "node_modules/.bin/mocha --opts test/mocha.opts" "preversion": "npm run prepublishOnly",
"push": "git push && git push origin \"v$npm_package_version\"",
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'",
"test": "nyc mocha --require source-map-support/register --require ts-node/register 'test/**/*.spec.ts'"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@gitlab.com:openstapps/proxy.git" "url": "git@gitlab.com:openstapps/proxy.git"
}, },
"author": "Anselm Stordeur <anselmstordeur@gmail.com>", "author": "Anselm Rochus Stordeur <anselmstordeur@gmail.com>",
"contributors": [ "contributors": [
"André Bierlein <andre.mt.bierlein@gmail.com>", "André Bierlein <andre.mt.bierlein@gmail.com>",
"Karl-Philipp Wulfert <krlwlfrt@gmail.com>", "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
"Benjamin Joeckel", "Benjamin Joeckel",
"Jovan Krunic <jovan.krunic@gmail.com>" "Jovan Krunic <jovan.krunic@gmail.com>",
"Rainer Killinger"
], ],
"license": "AGPL-3.0-only" "license": "AGPL-3.0-only",
"openstappsConfiguration": {
"forPackaging": false,
"ignoreCiEntries": [
"package"
]
},
"nyc": {
"all": true,
"branches": 95,
"check-coverage": true,
"exclude": [
"src/cli.ts"
],
"extension": [
".ts"
],
"functions": 95,
"include": [
"src"
],
"lines": 95,
"per-file": true,
"reporter": [
"html",
"text-summary"
],
"statements": 95
}
} }

View File

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

View File

@@ -14,48 +14,99 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Logger} from '@openstapps/logger'; 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(); // set transport on logger
export const logger = new Logger(SMTP.getInstance()); 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 * A representation of the file paths of the needed ssl certificates
*/ */
export interface SSLFilePaths { export interface SSLFilePaths {
certificate: string; /**
certificateChain: string; * Path to SSL certificate
certificateKey: string; */
dhparam: string; 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 * A representation of the config file
*/ */
export interface ConfigFile { export interface ConfigFile {
activeVersions: string[]; /**
hiddenRoutes: string[]; * List of active version
outdatedVersions: string[]; */
output: string; activeVersions: string[];
sslFilePaths: SSLFilePaths; /**
visibleRoutes: 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 * A view object to render the nginx config template
*/ */
export interface TemplateView { export interface TemplateView {
dockerVersionMap: string; /**
hiddenRoutes: string; * Docker version map
listener: string; */
staticRoute: string; dockerVersionMap: string;
visibleRoutes: string; /**
* Hidden routes
*/
hiddenRoutes: string;
/**
* Listener
*/
listener: string;
/**
* Static route
*/
staticRoute: string;
/**
* Visible routes
*/
visibleRoutes: string;
} }
/** /**
* Nginx protocol parameters to harden serverside settings * 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 Strict-Transport-Security "max-age=63072000; includeSubDomains;";
add_header X-Frame-Options DENY; add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff; 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 * Nginx ssl parameters to harden serverside settings
*/ */
export const sslHardeningParameters: string = ` export const sslHardeningParameters = `
ssl_protocols TLSv1.2 TLSv1.3; 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_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; ssl_prefer_server_ciphers on;
@@ -75,3 +126,17 @@ ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; ssl_session_tickets off;
ssl_stapling on; ssl_stapling on;
ssl_stapling_verify 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 * 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as config from 'config'; import {Logger} from '@openstapps/logger';
import * as Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import {existsSync, readFile} from 'fs-extra';
import {render} from 'mustache'; import {render} from 'mustache';
import {join} from 'path'; import {join} from 'path';
import { import {
ConfigFile, asyncReadFile,
logger, configFile,
isFileType,
protocolHardeningParameters, protocolHardeningParameters,
SSLFilePaths, SSLFilePaths,
sslHardeningParameters, sslHardeningParameters,
TemplateView, TemplateView,
} from './common'; } from './common';
const configFile: ConfigFile = config.util.toObject();
/** /**
* Checks if a ContainerInfo matches a name and version regex * Checks if a ContainerInfo matches a name and version regex
* @param {string} name *
* @param {RegExp} versionRegex * @param name Name to check
* @param {Dockerode.ContainerInfo} container * @param versionRegex Version regex to check
* @return {boolean} * @param container Container info for check
*/ */
export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): boolean { export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): boolean {
return typeof container.Labels['stapps.version'] === 'string' && return typeof container.Labels['stapps.version'] === 'string'
container.Labels['stapps.version'].match(versionRegex) !== null && && container.Labels['stapps.version'].match(versionRegex) !== null
typeof container.Labels['com.docker.compose.service'] === 'string' && && typeof container.Labels['com.docker.compose.service'] === 'string'
container.Labels['com.docker.compose.service'] === name; && 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. * 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) { if (container.Ports.length === 0) {
logger.error( await Logger.error(`Container ${container.Id} does not advertise any port.
'Container', Please expose a port if the container should be accessible by NGINX.`);
container.Id,
'does not advertise any Port. Please expose a Port if the container should be accessible by NGINX.',
);
return ''; 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 * Generates an upstream map
* @param {string[]} activeVersions *
* @param {string[]} outdatedVersions * It maps all stapps-backend-containers to an gateway.
* @param {Dockerode.ContainerInfo[]} containers *
* @return {string} * @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[], activeVersions: string[],
outdatedVersions: string[], outdatedVersions: string[],
containers: Dockerode.ContainerInfo[], containers: Dockerode.ContainerInfo[],
): string { ): Promise<string> {
let result = 'map $http_x_stapps_version $proxyurl {\n default unsupported;\n'; let result = 'map $http_x_stapps_version $proxyurl {\n default unsupported;\n';
let upstreams = ''; let upstreams = '';
let foundMatchingContainer = false; let foundMatchingContainer = false;
// active versions // active versions
result += activeVersions.map((activeVersionRegex) => { result += (await Promise.all(
const upstreamName = activeVersionRegex.replace(/[\\|\.|\+]/g, '_'); activeVersions
.map(async (activeVersionRegex) => {
const upstreamName = activeVersionRegex.replace(/[\\|.+]/g, '_');
const activeBackends = containers.filter((container) => { const activeBackends = containers.filter((container) => {
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), 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) { const gateWayOfContainer = await getGatewayOfStAppsBackend(activeBackends[0]);
throw new Error('Multiple backends for one version found.');
}
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`; return ` \"~${activeVersionRegex}\" unavailable;\n`;
} }),
} else { )).join('');
logger.error('No backend for version', activeVersionRegex, 'found');
return ` \"~${activeVersionRegex}\" unavailable;\n`;
}
}).join('');
// outdated versions // outdated versions
result += outdatedVersions.map((outdatedVersionRegex) => { result += outdatedVersions
return ` \"~${outdatedVersionRegex}\" outdated;`; .map((outdatedVersionRegex) => {
}).join('') + '\n\}'; return ` \"~${outdatedVersionRegex}\" outdated;`;
})
.join('');
result += '\n\}';
if (!foundMatchingContainer) { if (!foundMatchingContainer) {
logger.error( await Logger.error(
'No container with matching version label found. Please start a container with a matching version Label.', '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 * Generates http or https listener
* @param sslFiles
* @returns {string}
*/ */
function generateListener(sslFilePaths: SSLFilePaths) { export 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);
}
let listener = ''; let listener = '';
if (typeof sslFilePaths !== 'undefined' && if (typeof sslFilePaths !== 'undefined' &&
typeof sslFilePaths.certificate === 'string' && isSSLCert(sslFilePaths.certificate) && typeof sslFilePaths.certificate !== 'undefined' && isFileType(sslFilePaths.certificate,'crt') &&
typeof sslFilePaths.certificateChain === 'string' && isSSLCert(sslFilePaths.certificate) && typeof sslFilePaths.certificateChain !== 'undefined' && isFileType(sslFilePaths.certificateChain,'crt') &&
typeof sslFilePaths.certificateKey === 'string' && isSSLKey(sslFilePaths.certificate) && typeof sslFilePaths.certificateKey !== 'undefined' && isFileType(sslFilePaths.certificateKey,'key') &&
typeof sslFilePaths.dhparam === 'string' && isPEMFile(sslFilePaths.dhparam) typeof sslFilePaths.dhparam !== 'undefined' && isFileType(sslFilePaths.dhparam,'pem')
) { ) {
// https listener // https listener
listener = 'listen 443 ssl default_server;\n' + listener = `listen 443 ssl default_server;
`ssl_certificate ${sslFilePaths.certificate};\n` + ssl_certificate ${sslFilePaths.certificate};
`ssl_certificate_key ${sslFilePaths.certificateKey};\n` + ssl_certificate_key ${sslFilePaths.certificateKey};
`ssl_trusted_certificate ${sslFilePaths.certificateChain};\n` + ssl_trusted_certificate ${sslFilePaths.certificateChain};
`ssl_dhparam ${sslFilePaths.dhparam};\n` + ssl_dhparam ${sslFilePaths.dhparam};
`${sslHardeningParameters}`; ${sslHardeningParameters}`;
} else { } else {
// default http listener // default http listener
listener = 'listen 80 default_server;'; 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; return listener;
} }
/** /**
* Render a mustache template file with given view object * Render a mustache template file with given view object
* @param path (path to file) *
* @param view * @param path Path to template file
* @param callback * @param view Data to render template with
*/ */
async function renderTemplate(path: string, view: any): Promise<string> { async function renderTemplate(path: string, view: unknown): Promise<string> {
const content = await readFile(path, 'utf8'); const content = await asyncReadFile(path, 'utf8');
return render(content, view); return render(content, view);
} }
/** /**
* Returns view for nginx config file * Returns view for nginx config file
* @param containers *
* @param containers List of container info
*/ */
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> { 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(async (route) => {
const visibleRoutesPromises = configFile.visibleRoutes.map((route) => {
return renderTemplate(join('fixtures', 'visibleRoute.template'), { return renderTemplate(join('fixtures', 'visibleRoute.template'), {
cors, cors,
route, route,
}); });
}); });
const hiddenRoutesPromises = configFile.hiddenRoutes.map((route) => { const hiddenRoutesPromises = configFile.hiddenRoutes.map(async (route) => {
return renderTemplate(join('fixtures', 'hiddenRoute.template'), { return renderTemplate(join('fixtures', 'hiddenRoute.template'), {
cors, cors,
route, route,
@@ -202,7 +199,7 @@ export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Pr
}); });
return { return {
dockerVersionMap: generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers), dockerVersionMap: await generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''), hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''),
listener: generateListener(configFile.sslFilePaths), listener: generateListener(configFile.sslFilePaths),
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}), 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 * 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[]> { export async function getContainers(pathToDockerSocket = '/var/run/docker.sock'): Promise<Dockerode.ContainerInfo[]> {
const docker = new Dockerode({ const docker = new Dockerode({
@@ -221,12 +219,15 @@ export async function getContainers(pathToDockerSocket = '/var/run/docker.sock')
const containers = await docker.listContainers(); const containers = await docker.listContainers();
/* istanbul ignore next */
if (containers.length === 0) { if (containers.length === 0) {
throw new Error( throw new Error(
'No running docker containers found.' + `No running docker containers found.
`Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
); );
} }
/* istanbul ignore next */
return containers; return containers;
} }

View File

@@ -1,207 +0,0 @@
import {Logger} from '@openstapps/logger';
import {expect} from 'chai';
import {ContainerInfo} from 'dockerode';
import {slow, suite, test, timeout} from 'mocha-typescript';
import * as sinon from 'sinon';
import {containerMatchesRegex, generateUpstreamMap, getGatewayOfStAppsBackend} from '../src/main';
const logger = new Logger();
process.on('unhandledRejection', (err) => {
logger.error('UNHANDLED REJECTION', err.stack);
process.exit(1);
});
@suite(timeout(1000), slow(500))
// @ts-ignore
class ContainerInfoParsing {
static anyContainerWithExposedPorts: ContainerInfo;
static backendContainerWithExposedPorts: ContainerInfo;
static before(done: () => void) {
// tslint:disable:object-literal-sort-keys
this.backendContainerWithExposedPorts = {
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Names: [
'/deployment_backend_1',
],
Image: 'gitlab-registry.tubit.tu-berlin.de/stapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Command: 'node ./bin/www',
Created: 1524669882,
Ports: [
{
IP: '127.0.0.1',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
Labels: {
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
'com.docker.compose.container-number': '1',
'com.docker.compose.oneoff': 'False',
'com.docker.compose.project': 'deployment',
'com.docker.compose.service': 'backend',
'com.docker.compose.version': '1.21.0',
'stapps.version': '1.0.0',
},
State: 'running',
Status: 'Up 3 minutes',
HostConfig: {
NetworkMode: 'deployment_default',
},
NetworkSettings: {
Networks: {
deployment_default: {
IPAMConfig: null,
Links: null,
Aliases: null,
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
MacAddress: '03:41:ac:11:00:23',
},
},
},
};
// tslint:disable:object-literal-sort-keys
this.anyContainerWithExposedPorts = {
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Names: [
'/container_name_1',
],
Image: 'ubuntu:4',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Command: 'sh',
Created: 1524669882,
Ports: [
{
IP: '0.0.0.0',
PrivatePort: 80,
PublicPort: 80,
Type: 'tcp',
},
],
Labels: {},
State: 'running',
Status: 'Up 3 minutes',
HostConfig: {
NetworkMode: 'default',
},
NetworkSettings: {
Networks: {
bridge: {
IPAMConfig: null,
Links: null,
Aliases: null,
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
MacAddress: '03:41:ac:11:00:23',
},
},
},
};
done();
}
// tslint:disable:no-unused-expression
@test
checkIfContainerDoesNotMatchAnyContainer(done: () => void) {
expect(containerMatchesRegex(
'anyName',
new RegExp('d+'),
ContainerInfoParsing.anyContainerWithExposedPorts),
).to.be.false;
done();
}
@test
checkIfContainerDoesNotMatchIfVersionIsIncorrect(done: () => void) {
expect(containerMatchesRegex(
'backend',
new RegExp('1\\.4\\.\\d+'),
ContainerInfoParsing.backendContainerWithExposedPorts),
).to.be.false;
done();
}
@test
checkIfContainerMatches(done: () => void) {
expect(containerMatchesRegex(
'backend',
new RegExp('1\\.0\\.\\d+'),
ContainerInfoParsing.backendContainerWithExposedPorts),
).to.be.true;
done();
}
@test
getGatewayOfAnyContainerWithExposedPorts(done: () => void) {
expect(getGatewayOfStAppsBackend(ContainerInfoParsing.anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80');
done();
}
@test
getGatewayOfBackendContainer(done: () => void) {
expect(getGatewayOfStAppsBackend(ContainerInfoParsing.backendContainerWithExposedPorts))
.to
.be
.equal('127.0.0.1:3000');
done();
}
@test
upstreamMapCallsLoggerErrorWhenNoMatchingContainerIsFound(done: () => void) {
const stub = sinon.stub(console, 'error');
expect(generateUpstreamMap(
['0\\.8\\.\\d+'],
['1\\.1\\.\\d+'],
[ContainerInfoParsing.backendContainerWithExposedPorts],
)).to.be.equal('map $http_x_stapps_version $proxyurl {\n' +
' default unsupported;\n' +
' "~0\\.8\\.\\d+" unavailable;\n' +
' "~1\\.1\\.\\d+" outdated;\n' +
'}\n');
stub.restore();
expect(stub.args[0][0]).contains('[ERROR] No backend for version').and.contains('found');
done();
}
@test
upstreamMapWithOneActiveVersionAndNoOutdatedOnes(done: () => void) {
expect(
generateUpstreamMap(
['1\\.0\\.\\d+'],
['0\\.8\\.\\d+'],
[ContainerInfoParsing.backendContainerWithExposedPorts],
),
).to.be.equal(
'map $http_x_stapps_version $proxyurl {\n' +
' default unsupported;\n' +
' "~1\\.0\\.\\d+" 1__0___d_;\n' +
' "~0\\.8\\.\\d+" outdated;\n' +
'}\n' +
'upstream 1__0___d_ {\n' +
' server 127.0.0.1:3000;\n' +
'}\n',
);
done();
}
}

54
test/common.spec.ts Normal file
View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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/>.
*/
// tslint:disable:no-implicit-dependencies
// tslint:disable:no-magic-numbers
// tslint:disable:completed-docs
// tslint:disable:prefer-function-over-method
// tslint:disable:newline-per-chained-call
import {suite, test} from '@testdeck/mocha';
import {Logger} from '@openstapps/logger';
import {expect} from 'chai';
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
import { resolve } from 'path';
import { isFileType } from '../src/common';
process.on('unhandledRejection', async (error) => {
await Logger.error(error);
process.exit(1);
});
@suite
export class CommonSpec {
@test
async testSSLCert() {
const testCertDir = resolve(__dirname, 'certs');
mkdirSync(testCertDir);
const notAnExptectedFileTypeFilePath = resolve(testCertDir, 'notAnExptectedFileType.txt');
const anExptectedFileTypeFilePath = resolve(testCertDir, 'notARealCert.crt');
writeFileSync(notAnExptectedFileTypeFilePath,'Test');
writeFileSync(anExptectedFileTypeFilePath,'Test');
expect(isFileType(notAnExptectedFileTypeFilePath,'crt')).to.equal(false)
expect(isFileType(anExptectedFileTypeFilePath,'crt')).to.equal(true)
unlinkSync(notAnExptectedFileTypeFilePath);
unlinkSync(anExptectedFileTypeFilePath);
rmdirSync(testCertDir);
}
}

322
test/main.spec.ts Normal file
View File

@@ -0,0 +1,322 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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/>.
*/
// tslint:disable:no-implicit-dependencies
// tslint:disable:no-magic-numbers
// tslint:disable:completed-docs
// tslint:disable:prefer-function-over-method
// tslint:disable:newline-per-chained-call
import {Logger} from '@openstapps/logger';
import chai from 'chai';
import {expect} from 'chai';
import chaiSpies from 'chai-spies';
import {ContainerInfo} from 'dockerode';
import {slow, suite, test, timeout} from '@testdeck/mocha';
import {sslHardeningParameters, protocolHardeningParameters, SSLFilePaths } from './../src/common';
import {containerMatchesRegex, generateUpstreamMap, getGatewayOfStAppsBackend, getTemplateView, generateListener, getContainers} from '../src/main';
import { resolve } from 'path';
import { mkdirSync, writeFileSync, unlinkSync, rmdirSync } from 'fs';
process.on('unhandledRejection', async (error) => {
await Logger.error(error);
process.exit(1);
});
chai.should();
chai.use(chaiSpies);
@suite(timeout(1000), slow(500))
export class MainSpec {
static anyContainerWithExposedPorts: ContainerInfo = {
Command: 'sh',
Created: 1524669882,
HostConfig: {
NetworkMode: 'default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'ubuntu:4',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {},
Mounts: [],
Names: [
'/container_name_1',
],
NetworkSettings: {
Networks: {
bridge: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: '0.0.0.0',
PrivatePort: 80,
PublicPort: 80,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};
static backendContainerWithExposedPorts: ContainerInfo = {
Command: 'node ./bin/www',
Created: 1524669882,
HostConfig: {
NetworkMode: 'deployment_default',
},
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
Image: 'gitlab-registry.tubit.tu-berlin.de/stapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
'com.docker.compose.container-number': '1',
'com.docker.compose.oneoff': 'False',
'com.docker.compose.project': 'deployment',
'com.docker.compose.service': 'backend',
'com.docker.compose.version': '1.21.0',
'stapps.version': '1.0.0',
},
Mounts: [],
Names: [
'/deployment_backend_1',
],
NetworkSettings: {
Networks: {
deployment_default: {
Aliases: null,
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
Gateway: '172.18.0.1',
GlobalIPv6Address: '',
GlobalIPv6PrefixLen: 0,
IPAMConfig: null,
IPAddress: '172.18.0.3',
IPPrefixLen: 16,
IPv6Gateway: '',
Links: null,
MacAddress: '03:41:ac:11:00:23',
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
},
},
},
Ports: [
{
IP: '127.0.0.1',
PrivatePort: 3000,
PublicPort: 3000,
Type: 'tcp',
},
],
State: 'running',
Status: 'Up 3 minutes',
};
static sandbox = chai.spy.sandbox();
before() {
MainSpec.sandbox.restore();
}
@test
checkIfContainerDoesNotMatchAnyContainer() {
expect(containerMatchesRegex(
'anyName',
new RegExp('d+'),
MainSpec.anyContainerWithExposedPorts),
).to.be.equal(false);
}
@test
checkIfContainerDoesNotMatchIfVersionIsIncorrect() {
expect(containerMatchesRegex(
'backend',
new RegExp('1\\.4\\.\\d+'),
MainSpec.backendContainerWithExposedPorts),
).to.be.equal(false);
}
@test
checkIfContainerMatches() {
expect(containerMatchesRegex(
'backend',
new RegExp('1\\.0\\.\\d+'),
MainSpec.backendContainerWithExposedPorts),
).to.be.equal(true);
}
@test
async getGatewayOfAnyContainerWithExposedPorts() {
expect(await getGatewayOfStAppsBackend(MainSpec.anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80');
}
@test
async 'get gateway of backend container'() {
const spy = MainSpec.sandbox.on(console, 'error', () => {
// noop
});
const containerWithoutPorts: Partial<ContainerInfo> = {
Id: 'Foo',
Ports: [],
};
expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal('');
expect(spy.__spy.calls[0][0]).to.contain('Container Foo does not advertise any port.');
}
@test
async 'get gateway of backend container without ports'() {
expect(await getGatewayOfStAppsBackend(MainSpec.backendContainerWithExposedPorts)).to.be.equal('127.0.0.1:3000');
}
@test
async upstreamMapCallsLoggerErrorWhenNoMatchingContainerIsFound() {
const spy = MainSpec.sandbox.on(console, 'error', () => {
});
expect(await generateUpstreamMap(
['0\\.8\\.\\d+'],
['1\\.1\\.\\d+'],
[MainSpec.backendContainerWithExposedPorts],
)).to.be.equal(`map $http_x_stapps_version $proxyurl {
default unsupported;
"~0\\.8\\.\\d+" unavailable;
"~1\\.1\\.\\d+" outdated;
}
`);
expect(spy.__spy.calls[0][0]).to.contain('No backend for version');
}
@test
async upstreamMapWithOneActiveVersionAndNoOutdatedOnes() {
expect(await generateUpstreamMap(
['1\\.0\\.\\d+'],
['0\\.8\\.\\d+'],
[MainSpec.backendContainerWithExposedPorts],
)).to.be.equal(`map $http_x_stapps_version $proxyurl {
default unsupported;
"~1\\.0\\.\\d+" 1__0___d_;
"~0\\.8\\.\\d+" outdated;
}
upstream 1__0___d_ {
server 127.0.0.1:3000;
}
`);
}
@test
async testGetContainers() {
try {
await getContainers();
return false;
} catch (e) {
if (e.message.startsWith('No')) {
// Result, if docker is installed
expect(e.message).to.equal(`No running docker containers found.
Please check if docker is running and Node.js can access the docker socket (/var/run/docker.sock)`);
} else {
// Result, if docker is not installed
expect(e.message).to.equal(`connect ENOENT /var/run/docker.sock`);
}
}
return true;
}
@test
async testGetTemplateView() {
try {
let containersWithSameVersion = [MainSpec.backendContainerWithExposedPorts, MainSpec.backendContainerWithExposedPorts];
await getTemplateView(containersWithSameVersion);
return false;
} catch (e) {
expect(e.message).to.equal(
`Multiple backends for one version found.`);
}
return true;
}
@test
createListenerFaultyConfig() {
expect(generateListener({
certificate: 'faultyTest',
certificateChain: 'faultyTest',
certificateKey: 'faultyTest',
dhparam: 'faultyTest',
})).to
.equal(`listen 80 default_server;
${protocolHardeningParameters}
`);
}
@test
createListenerCorrectConfig() {
const testCertDir = resolve(__dirname, 'certs');
mkdirSync(testCertDir);
const certificateFile = resolve(testCertDir, 'ssl.crt');
const certificateKeyFile = resolve(testCertDir, 'ssl.key');
const certificateChainFile = resolve(testCertDir, 'chain.crt');
const dhparamFile = resolve(testCertDir, 'dhparam.pem');
writeFileSync(certificateFile,'Test');
writeFileSync(certificateKeyFile,'Test');
writeFileSync(certificateChainFile,'Test');
writeFileSync(dhparamFile,'Test');
const sslFilePaths: SSLFilePaths = {
certificate: certificateFile,
certificateKey: certificateKeyFile,
certificateChain: certificateChainFile,
dhparam: dhparamFile,
};
expect(generateListener(sslFilePaths)).to.equal(`listen 443 ssl default_server;
ssl_certificate ${sslFilePaths.certificate};
ssl_certificate_key ${sslFilePaths.certificateKey};
ssl_trusted_certificate ${sslFilePaths.certificateChain};
ssl_dhparam ${sslFilePaths.dhparam};
${sslHardeningParameters}
${protocolHardeningParameters}
`);
unlinkSync(certificateFile);
unlinkSync(certificateKeyFile);
unlinkSync(certificateChainFile);
unlinkSync(dhparamFile);
rmdirSync(testCertDir);
}
}

View File

@@ -1,4 +0,0 @@
--ui mocha-typescript
--require ts-node/register
--require source-map-support/register
test/*.ts