/* eslint-disable @typescript-eslint/no-explicit-any */ /* * 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 . */ import {Logger} from '@openstapps/logger'; import chai from 'chai'; import {expect} from 'chai'; import {ContainerInfo} from 'dockerode'; import { sslHardeningParameters, protocolHardeningParameters, SSLFilePaths, configFile, } from './../src/common.js'; import { containerMatchesRegex, generateUpstreamMap, getGatewayOfStAppsBackend, getTemplateView, generateListener, generateMetricsServer, getContainers, } from '../src/main.js'; import path from 'path'; import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs'; import {anyContainerWithExposedPorts} from './containers/any-with-exposed-ports.js'; import {backendContainerWithExposedPorts} from './containers/backend-with-exposed-ports.js'; import {swarmBackendContainerWithExposedPorts} from './containers/swarm-backend-with-exposed-ports.js'; import {fileURLToPath} from 'url'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import portScannerModule from '../src/port-scanner.js'; process.on('unhandledRejection', async error => { await Logger.error(error); process.exit(1); }); chai.should(); chai.use(sinonChai); console.log(configFile); describe('main', function () { this.timeout(1000); this.slow(500); const sandbox = sinon.createSandbox(); beforeEach(function () { sandbox.restore(); }); it('should check if container does not match any container', function () { expect(containerMatchesRegex('anyName', new RegExp('d+'), anyContainerWithExposedPorts)).to.be.equal( false, ); }); it('should check if container does not match if version is incorrect', function () { expect( containerMatchesRegex('backend', new RegExp('1\\.4\\.\\d+'), backendContainerWithExposedPorts), ).to.be.equal(false); }); it('should check if container matches', function () { expect( containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), backendContainerWithExposedPorts), ).to.be.equal(true); expect( containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), swarmBackendContainerWithExposedPorts), ).to.be.equal(true); }); it('should get gateway of any container with exposed ports', async function () { expect(await getGatewayOfStAppsBackend(anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80'); }); it('should get gateway of backend container', async function () { const spy = sandbox.stub(console, 'error'); const containerWithoutPorts: Partial = { Id: 'Foo', Ports: [], Names: ['/container_name_1'], }; expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal(''); expect(spy).to.have.been.calledWithMatch('Container /container_name_1 does not advertise any port.'); }); it('should get gateway of backend container without ports', async function () { expect(await getGatewayOfStAppsBackend(backendContainerWithExposedPorts)).to.be.equal('127.0.0.1:3000'); }); it('should get gateway of backend container within docker swarm', async function () { const backendContainer = swarmBackendContainerWithExposedPorts as any; delete backendContainer.Ports[0].IP; const spy = sandbox.stub(portScannerModule, 'isPortFree').resolves(true); expect(await getGatewayOfStAppsBackend(backendContainer)).to.be.equal('172.18.0.3:3000'); expect(spy).to.have.been.called; }); it('should fail to get gateway of backend container if unreachable', async function () { const backendContainer = swarmBackendContainerWithExposedPorts as any; delete backendContainer.Ports[0].IP; const spy = sandbox.stub(console, 'error'); const scanner = sandbox.stub(portScannerModule, 'isPortFree').resolves(false); expect(await getGatewayOfStAppsBackend(swarmBackendContainerWithExposedPorts)).to.be.equal(''); expect(scanner.calledOnce).to.be.true; expect(spy).to.have.been.calledWithMatch( "It's possible your current Docker network setup isn't supported yet.", ); }); it('should fail to get gateway of backend container network mode is unsupported', async function () { const backendContainer = swarmBackendContainerWithExposedPorts as any; delete backendContainer.Ports[0].IP; delete backendContainer.Ports[0].PublicPort; delete backendContainer.Ports[0].PrivatePort; const spy = sandbox.stub(console, 'error'); expect(await getGatewayOfStAppsBackend(swarmBackendContainerWithExposedPorts)).to.be.equal(''); expect(spy).to.have.been.calledWithMatch( "It's possible your current Docker network setup isn't supported yet.", ); }); it('should upstream map calls logger error when no matching container is found', async function () { const spy = sandbox.stub(console, 'warn'); expect(await generateUpstreamMap(['0\\.8\\.\\d+'], ['1\\.1\\.\\d+'], [backendContainerWithExposedPorts])) .to.be.equal(`map $http_x_stapps_version $proxyurl { default unsupported; "~0\\.8\\.\\d+" unavailable; "~1\\.1\\.\\d+" outdated; } `); expect(spy).to.have.been.calledWithMatch('No backend for version'); }); it('should upstream map with one active version and no outdated ones', async function () { expect(await generateUpstreamMap(['1\\.0\\.\\d+'], ['0\\.8\\.\\d+'], [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; } `); }); it('should get containers', async function () { try { await getContainers(); return false; } catch (error) { if ((error as Error).message.startsWith('No')) { // Result, if docker is installed expect((error 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((error as Error).message); } } return true; }); it('should get template view', async function () { try { const containersWithSameVersion = [backendContainerWithExposedPorts, backendContainerWithExposedPorts]; await getTemplateView(containersWithSameVersion); return false; } catch (error) { expect((error as Error).message).to.equal(`Multiple backends for one version found.`); } return true; }); it('should include metrics config', async function () { expect(await generateMetricsServer('test', true)).length.to.be.greaterThan(1); }); it('should omit metrics config', async function () { expect(await generateMetricsServer('test', false)).to.equal(''); }); it('should create listener faulty config', async function () { expect( generateListener({ certificate: 'faultyTest', certificateChain: 'faultyTest', certificateKey: 'faultyTest', dhparam: 'faultyTest', }), ).to.equal(`listen 80 default_server; ${protocolHardeningParameters} `); }); it('should create listener correct config', async function () { const testCertDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'certs'); mkdirSync(testCertDirectory); const certificateFile = path.resolve(testCertDirectory, 'ssl.crt'); const certificateKeyFile = path.resolve(testCertDirectory, 'ssl.key'); const certificateChainFile = path.resolve(testCertDirectory, 'chain.crt'); const dhparamFile = path.resolve(testCertDirectory, '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(testCertDirectory); }); });