mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
233 lines
7.4 KiB
TypeScript
233 lines
7.4 KiB
TypeScript
/*
|
|
* 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/>.
|
|
*/
|
|
import * as config from 'config';
|
|
import * as Dockerode from 'dockerode';
|
|
import {existsSync, readFile} from 'fs-extra';
|
|
import {render} from 'mustache';
|
|
import {join} from 'path';
|
|
import {
|
|
ConfigFile,
|
|
logger,
|
|
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}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
export function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): 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.',
|
|
);
|
|
return '';
|
|
} else {
|
|
// 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}
|
|
*/
|
|
export function generateUpstreamMap(
|
|
activeVersions: string[],
|
|
outdatedVersions: string[],
|
|
containers: Dockerode.ContainerInfo[],
|
|
): 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, '_');
|
|
|
|
const activeBackends = containers.filter((container) => {
|
|
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
|
|
});
|
|
|
|
if (activeBackends.length > 0) {
|
|
|
|
foundMatchingContainer = true;
|
|
|
|
if (activeBackends.length > 1) {
|
|
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`;
|
|
} else {
|
|
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
|
}
|
|
} else {
|
|
logger.error('No backend for version', activeVersionRegex, 'found');
|
|
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
|
}
|
|
}).join('');
|
|
|
|
// outdated versions
|
|
result += outdatedVersions.map((outdatedVersionRegex) => {
|
|
return ` \"~${outdatedVersionRegex}\" outdated;`;
|
|
}).join('') + '\n\}';
|
|
|
|
if (!foundMatchingContainer) {
|
|
logger.error(
|
|
'No container with matching version label found. Please start a container with a matching version Label.',
|
|
);
|
|
}
|
|
|
|
return `${result}${upstreams}\n`;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
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)
|
|
) {
|
|
// 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}`;
|
|
} else {
|
|
// default http listener
|
|
listener = 'listen 80 default_server;';
|
|
logger.warn('Https usage is not setup properly, falling back to http!');
|
|
}
|
|
listener = `${listener}\n${protocolHardeningParameters}\n`;
|
|
return listener;
|
|
}
|
|
|
|
/**
|
|
* Render a mustache template file with given view object
|
|
* @param path (path to file)
|
|
* @param view
|
|
* @param callback
|
|
*/
|
|
async function renderTemplate(path: string, view: any): Promise<string> {
|
|
const content = await readFile(path, 'utf8');
|
|
return render(content, view);
|
|
}
|
|
|
|
/**
|
|
* Returns view for nginx config file
|
|
* @param containers
|
|
*/
|
|
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
|
|
|
|
const cors = await readFile('./fixtures/cors.template', 'utf8');
|
|
|
|
const visibleRoutesPromises = configFile.visibleRoutes.map((route) => {
|
|
return renderTemplate(join('fixtures', 'visibleRoute.template'), {
|
|
cors,
|
|
route,
|
|
});
|
|
});
|
|
|
|
const hiddenRoutesPromises = configFile.hiddenRoutes.map((route) => {
|
|
return renderTemplate(join('fixtures', 'hiddenRoute.template'), {
|
|
cors,
|
|
route,
|
|
});
|
|
});
|
|
|
|
return {
|
|
dockerVersionMap: generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
|
|
hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''),
|
|
listener: generateListener(configFile.sslFilePaths),
|
|
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}),
|
|
visibleRoutes: (await Promise.all(visibleRoutesPromises)).join(''),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read the list of docker containers
|
|
* @param pathToDockerSocket
|
|
*/
|
|
export async function getContainers(pathToDockerSocket = '/var/run/docker.sock'): Promise<Dockerode.ContainerInfo[]> {
|
|
const docker = new Dockerode({
|
|
socketPath: pathToDockerSocket,
|
|
});
|
|
|
|
const containers = await docker.listContainers();
|
|
|
|
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})`,
|
|
);
|
|
}
|
|
|
|
return containers;
|
|
}
|