/* * 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 . */ // 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: 'registry.gitlab.com/openstapps/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 'check if container does not match any container'() { expect(containerMatchesRegex( 'anyName', new RegExp('d+'), MainSpec.anyContainerWithExposedPorts), ).to.be.equal(false); } @test 'check if container does not match if version is incorrect'() { expect(containerMatchesRegex( 'backend', new RegExp('1\\.4\\.\\d+'), MainSpec.backendContainerWithExposedPorts), ).to.be.equal(false); } @test 'check if container matches'() { expect(containerMatchesRegex( 'backend', new RegExp('1\\.0\\.\\d+'), MainSpec.backendContainerWithExposedPorts), ).to.be.equal(true); } @test async 'get gateway of any container with exposed ports'() { 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 = { 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 'upstream map calls logger error when no matching container is found'() { 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 'upstream map with one active version and no outdated ones'() { 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 'get containers'() { try { await getContainers(); return false; } catch (e) { if ((e as Error).message.startsWith('No')) { // Result, if docker is installed expect((e as Error).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([ new Error(`connect ENOENT /var/run/docker.sock`).message, new Error('connect EACCES /var/run/docker.sock').message, ]).to.include((e as Error).message); } } return true; } @test async 'get template view'() { try { let containersWithSameVersion = [MainSpec.backendContainerWithExposedPorts, MainSpec.backendContainerWithExposedPorts]; await getTemplateView(containersWithSameVersion); return false; } catch (e) { expect((e as Error).message).to.equal( `Multiple backends for one version found.`); } return true; } @test 'create listener faulty config'() { expect(generateListener({ certificate: 'faultyTest', certificateChain: 'faultyTest', certificateKey: 'faultyTest', dhparam: 'faultyTest', })).to .equal(`listen 80 default_server; ${protocolHardeningParameters} `); } @test 'create listener correct config'() { 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); } }