diff --git a/Dockerfile.debug b/Dockerfile.debug index e0b985e..f27c116 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -3,11 +3,22 @@ FROM nginx:stable-alpine AS runner # Remove builtin configs from parent container RUN rm /etc/nginx/conf.d/* +COPY src/nginx/builtin /etc/nginx/conf.d WORKDIR /app -RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.11/main/ nodejs=12.22.6-r0 yarn +# Dependencies +# Bash +RUN apk add --no-cache bash && \ + # Node + apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.11/main/ nodejs=12.22.6-r0 && \ + # Certbot + apk add --no-cache certbot && \ + # Directory for certificates + mkdir -p /var/www/letsencrypt && \ + # Yarn for development + apk add --no-cache yarn ENV FORCE_COLOR 1 ENV DATA_PATH /app/data diff --git a/package.json b/package.json index 8487cb8..96bbad0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test": "jest", "lint": "eslint .", "ts": "ts-node -T src", - "dev": "tsnd --respawn -T --rs --cls -r tsconfig-paths/register --experimental-modules -- src", + "dev": "tsnd --respawn -T --rs --cls -r tsconfig-paths/register --experimental-modules -- src run", "prepare": "husky install" }, "dependencies": {}, diff --git a/src/index.ts b/src/index.ts index 7873b4b..b484f16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -167,24 +167,22 @@ const main = async ({ ); const sslServers = config.servers.filter((server) => !server.disable_cert); - const serversWithKeys = await filterServersWithValidSslFiles(sslServers); - const serversWithoutKeys = sslServers.filter( - (server) => !serversWithKeys.includes(server) - ); + const { validServers, invalidSslServers } = + await filterServersWithValidSslFiles(sslServers); promises.push( ...createConfigFiles( [ - ...serversWithKeys, // SSL enabled servers, with all files for it + ...validServers, // SSL enabled servers, with all files for it ...config.servers.filter((server) => server.disable_cert) // SSL Disabled Servers ], config.username ) ); - promises.push(certbot(serversWithoutKeys)); + promises.push(certbot(invalidSslServers)); - if (serversWithoutKeys.length) promises.push(createDHPemIfNotExists()); + if (invalidSslServers.length) promises.push(createDHPemIfNotExists()); // Make sure all certificate files are created await Promise.all(promises); @@ -193,7 +191,12 @@ const main = async ({ // Try again await Promise.all( createConfigFiles( - await filterServersWithValidSslFiles(serversWithoutKeys, true), + await ( + await filterServersWithValidSslFiles( + invalidSslServers.map(({ server }) => server), + true + ) + ).validServers, config.username ) ); diff --git a/src/lib/certbot.ts b/src/lib/certbot.ts index 98bee9f..67ce300 100644 --- a/src/lib/certbot.ts +++ b/src/lib/certbot.ts @@ -1,10 +1,9 @@ import execSh from "exec-sh"; import { logger } from "@lib/logger"; +import { InvalidSslServer } from "@utils/filterServersWithValidSslFiles"; import settings from "@utils/settings"; -import { SimpleServer } from "@models/ParsedConfig"; - const exec = execSh.promise; const hasMail = (): boolean => { @@ -33,23 +32,23 @@ const certbotDisabled = (): boolean => { return false; }; -export const certbot = async (servers: SimpleServer[]): Promise => { - if (!servers.length) { +export const certbot = async ( + invalidSslServers: InvalidSslServer[] +): Promise => { + if (!invalidSslServers.length) { return logger.allValid(); } if (certbotDisabled() || !hasMail() || !(await hasCertbot())) { return logger.skippingCertbot(); } - logger.requestingCertificates({ count: servers.length }); + logger.requestingCertificates({ count: invalidSslServers.length }); - const certNames = [ - ...new Set( - servers.map((server) => server.certbot_name ?? server.server_name) - ) - ]; + for (const invalidSsl of invalidSslServers) { + const certName = + invalidSsl.server.certbot_name ?? invalidSsl.server.server_name; + const force = invalidSsl.reason == "staging"; - for (const certName of certNames) { const command = createShellCommand("certbot certonly", { "agree-tos": "", keep: "", @@ -61,7 +60,8 @@ export const certbot = async (servers: SimpleServer[]): Promise => { "preferred-challenges": "http-01", email: settings.certbotMail, "non-interactive": "", - ...(settings.staging && { "test-cert": "" }) + ...(settings.staging && { "test-cert": "" }), + ...(force && { "force-renewal": "" }) }); await exec(command, true) @@ -71,8 +71,8 @@ export const certbot = async (servers: SimpleServer[]): Promise => { .then((output) => { if (output && output.stdout) { logger.certbotLog({ - index: certNames.indexOf(certName), - size: certNames.length, + index: invalidSslServers.indexOf(invalidSsl), + size: invalidSslServers.length, certificate: certName, log: output.stdout.split("\n")[0] }); diff --git a/src/lib/logMessages.ts b/src/lib/logMessages.ts index c180b8f..9b666c3 100644 --- a/src/lib/logMessages.ts +++ b/src/lib/logMessages.ts @@ -153,18 +153,18 @@ export const logMessages = defineLogList({ ], certificateValid: ({ serverName, - days = -1 + days = -1, + staging }: { serverName: string; days: number; + staging: boolean; }) => [ Log.info, Tag.certbot, - chalk`The certificate for {dim ${serverName}} is valid for {bold ${gradientNumber( - Math.round(days), - 0, - 90 - )}} days` + chalk`The certificate for {dim ${serverName}}${ + staging ? chalk` ({yellow staging})` : "" + } is valid for {bold ${gradientNumber(Math.round(days), 0, 90)}} days` ], certificateExpiry: ({ serverName, @@ -190,6 +190,11 @@ export const logMessages = defineLogList({ }}` ]; }, + certificateStaging: ({ serverName }: { serverName: string }) => [ + Log.info, + Tag.certbot, + chalk`{yellow The certificate for {dim ${serverName}} is using the {yellow Staging} environment, renewing for production.}` + ], certificateParseFailed: ({ file, error diff --git a/src/models/cert2json.d.ts b/src/models/cert2json.d.ts index 6d33c87..117e03f 100644 --- a/src/models/cert2json.d.ts +++ b/src/models/cert2json.d.ts @@ -5,6 +5,10 @@ declare module "cert2json" { notBefore: Date; notAfter: Date; }; + + issuer: { + full: string; + }; }; } export const parse: (certificate: string) => Certificate; diff --git a/src/tests/__snapshots__/log.test.ts.snap b/src/tests/__snapshots__/log.test.ts.snap index ce6e255..0a5e0c6 100644 --- a/src/tests/__snapshots__/log.test.ts.snap +++ b/src/tests/__snapshots__/log.test.ts.snap @@ -23,6 +23,7 @@ exports[`Logger All log Messages 1`] = ` "[NCM] [ERROR] [CERTBOT] The certificate files for undefined could not be created, please see the error above. This domain is now disabled.", "[NCM] [INFO] [CERTBOT] The certificate for undefined is valid for -1 days", "[NCM] [INFO] [CERTBOT] The certificate for undefined, expired 1 days ago", + "[NCM] [INFO] [CERTBOT] The certificate for undefined is using the Staging environment, renewing for production.", "[NCM] [ERROR] [CERTBOT] Parsing of the certificate undefined failed: undefined", "[NCM] [INFO] [DHPARAMS] Creating a 2048 bit Diffie-Hellman parameter... This could take a while.", "[NCM] [DONE] [DHPARAMS] Diffie-Hellman parameters are created successfully, took NaN seconds. /etc/letsencrypt/dhparams/dhparam.pem", diff --git a/src/tests/utils.test.ts b/src/tests/utils.test.ts index 8cee8a7..e28957f 100644 --- a/src/tests/utils.test.ts +++ b/src/tests/utils.test.ts @@ -8,7 +8,7 @@ import { createHash } from "@utils/createHash"; import { dnsLookup } from "@utils/dnsLookup"; import { fixedLength } from "@utils/fixedLength"; import { msToDays } from "@utils/msToDays"; -import { parseCertificateExpiry } from "@utils/parseCertificateExpiry"; +import { parseCertificateFile } from "@utils/parseCertificateFile"; import { parseIntDefault } from "@utils/parseIntDefault"; import { plural } from "@utils/plural"; import { sslFilesFor } from "@utils/sslFilesFor"; @@ -68,16 +68,18 @@ describe("Utilities", () => { test("Parse Certificate Expiry", async () => { const [exists, notExisting] = await Promise.all([ - parseCertificateExpiry( + parseCertificateFile( join(process.cwd(), "src", "tests", "certs", "example.pem") ), - parseCertificateExpiry("notExisting.pem") + parseCertificateFile("notExisting.pem") ]); expect(exists).not.toBe(false); expect(notExisting).toBe(false); if (exists !== false) - expect(exists.toUTC().toISO()).toEqual("2013-10-04T12:47:15.000Z"); + expect(exists.expiry.toISOString()).toEqual( + "2013-10-04T12:47:15.000Z" + ); }); test("Parse Integer with Default", () => { diff --git a/src/utils/filterServersWithValidSslFiles.ts b/src/utils/filterServersWithValidSslFiles.ts index 38e9b9d..ea3641e 100644 --- a/src/utils/filterServersWithValidSslFiles.ts +++ b/src/utils/filterServersWithValidSslFiles.ts @@ -1,12 +1,23 @@ import { pathExists } from "fs-extra"; import { logger } from "@lib/logger"; -import { parseCertificateExpiry } from "@utils/parseCertificateExpiry"; +import { msToDays } from "@utils/msToDays"; +import { parseCertificateFile } from "@utils/parseCertificateFile"; import settings from "@utils/settings"; import { sslFileFor, sslFilesFor } from "@utils/sslFilesFor"; import { SimpleServer } from "@models/ParsedConfig"; +export interface InvalidSslServer { + server: SimpleServer; + reason: "expired" | "staging" | "missing"; +} + +export interface FilteredServers { + validServers: SimpleServer[]; + invalidSslServers: InvalidSslServer[]; +} + /** * Returns a list of servers which has valid certificates * @@ -20,8 +31,9 @@ import { SimpleServer } from "@models/ParsedConfig"; export const filterServersWithValidSslFiles = async ( servers: SimpleServer[], last = false -): Promise => { - const out: SimpleServer[] = []; +): Promise => { + const validServers: SimpleServer[] = []; + const invalidSslServers: InvalidSslServer[] = []; server: for (const server of servers) { const sslFiles = sslFilesFor(server); @@ -42,35 +54,46 @@ export const filterServersWithValidSslFiles = async ( This setting allows for a config without certificate files If it is the second try, return all configs */ + if (settings.enableConfigMissingCerts) { - out.push(server); + validServers.push(server); + continue server; } } + invalidSslServers.push({ server, reason: "missing" }); continue server; } } - const expiry = await parseCertificateExpiry( + const certificate = await parseCertificateFile( sslFileFor(server, "cert.pem") ); - if (!expiry) continue server; - const days = expiry.diffNow().as("days"); + if (certificate == false) continue server; + const days = msToDays(certificate.expiry.valueOf() - Date.now()); if (days < 30) { // Certificate expires in less than 30 days logger.certificateExpiry({ serverName: server.server_name, - days: days + days }); - continue server; - } - logger.certificateValid({ serverName: server.server_name, days: days }); + invalidSslServers.push({ server, reason: "expired" }); + } else if (certificate.staging && settings.staging == false) { + console.log("Is Staging"); + invalidSslServers.push({ server, reason: "staging" }); + } else { + logger.certificateValid({ + serverName: server.server_name, + days, + staging: certificate.staging + }); - out.push(server); + validServers.push(server); + } } - return out; + return { invalidSslServers, validServers }; }; diff --git a/src/utils/parseCertificateExpiry.ts b/src/utils/parseCertificateFile.ts similarity index 60% rename from src/utils/parseCertificateExpiry.ts rename to src/utils/parseCertificateFile.ts index 10ae9ae..2d714de 100644 --- a/src/utils/parseCertificateExpiry.ts +++ b/src/utils/parseCertificateFile.ts @@ -1,18 +1,24 @@ import { parse } from "cert2json"; import { pathExists, readFile } from "fs-extra"; -import { DateTime } from "luxon"; import { logger } from "@lib/logger"; /** - * Checks the expiry of a certificate file + * Parses a certificate file + * Returns useful information + * or false if the file does not exist * @param certificateFile The path to the certificate - * @returns A {@link DateTime} when the certificate expires + * @returns An object containing expiry and whether the certificate is staging */ -export const parseCertificateExpiry = async ( +export interface ParsedCertificate { + expiry: Date; + staging: boolean; +} + +export const parseCertificateFile = async ( certificateFile: string -): Promise => { +): Promise => { if (!(await pathExists(certificateFile))) { logger.certificateParseFailed({ file: certificateFile, @@ -25,7 +31,10 @@ export const parseCertificateExpiry = async ( try { const cert = parse(certificate); - return DateTime.fromJSDate(cert.tbs.validity.notAfter); + return { + expiry: cert.tbs.validity.notAfter, + staging: cert.tbs.issuer.full.includes("STAGING") + }; } catch (error) { if (error instanceof Error) logger.certificateParseFailed({