Add mark as ready

This commit is contained in:
Fang-Pen Lin
2025-12-01 14:14:17 -08:00
parent cbf46e720d
commit 17f1922041
2 changed files with 91 additions and 73 deletions
@@ -1,5 +1,6 @@
import axios, { AxiosError } from "axios";
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange";
@@ -18,7 +19,11 @@ import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
type TPkiAcmeChallengeServiceFactoryDep = {
acmeChallengeDAL: Pick<
TPkiAcmeChallengeDALFactory,
"transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById"
| "transaction"
| "findByIdForChallengeValidation"
| "markAsValidCascadeById"
| "markAsInvalidCascadeById"
| "updateById"
>;
};
@@ -26,9 +31,8 @@ export const pkiAcmeChallengeServiceFactory = ({
acmeChallengeDAL
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig();
const validateChallengeResponse = async (challengeId: string): Promise<void> => {
const error: Error | undefined = await acmeChallengeDAL.transaction(async (tx) => {
const markChallengeAsyReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => {
return acmeChallengeDAL.transaction(async (tx) => {
logger.info({ challengeId }, "Validating ACME challenge response");
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
if (!challenge) {
@@ -52,11 +56,27 @@ export const pkiAcmeChallengeServiceFactory = ({
if (challenge.type !== AcmeChallengeType.HTTP_01) {
throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" });
}
let host = challenge.auth.identifierValue;
const host = challenge.auth.identifierValue;
// check if host is a private ip address
if (isPrivateIp(host)) {
throw new BadRequestError({ message: "Private IP addresses are not allowed" });
}
return acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Processing }, tx);
});
};
const validateChallengeResponse = async (challengeId: string): Promise<void> => {
logger.info({ challengeId }, "Validating ACME challenge response");
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId);
if (!challenge) {
throw new NotFoundError({ message: "ACME challenge not found" });
}
if (challenge.status !== AcmeChallengeStatus.Processing) {
throw new BadRequestError({
message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Processing}`
});
}
let host = challenge.auth.identifierValue;
if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) {
host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host];
logger.warn(
@@ -91,10 +111,10 @@ export const pkiAcmeChallengeServiceFactory = ({
if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) {
throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" });
}
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx);
await acmeChallengeDAL.markAsValidCascadeById(challengeId);
} catch (exp) {
// TODO: we should retry the challenge validation a few times, but let's keep it simple for now
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx);
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId);
// Properly type and inspect the error
if (axios.isAxiosError(exp)) {
const axiosError = exp as AxiosError;
@@ -102,31 +122,26 @@ export const pkiAcmeChallengeServiceFactory = ({
const errorMessage = axiosError.message;
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
return new AcmeConnectionError({ message: "Connection refused" });
throw new AcmeConnectionError({ message: "Connection refused" });
}
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
logger.error(exp, "Connection timed out while validating ACME challenge response");
return new AcmeConnectionError({ message: "Connection timed out" });
throw new AcmeConnectionError({ message: "Connection timed out" });
}
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
}
if (exp instanceof Error) {
logger.error(exp, "Error validating ACME challenge response");
} else {
throw exp;
}
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
}
return exp;
}
});
if (error) {
throw error;
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
}
};
return { validateChallengeResponse };
return { markChallengeAsyReady, validateChallengeResponse };
};
@@ -1,6 +1,8 @@
import { JWSHeaderParameters } from "jose";
import { z } from "zod";
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
import {
AcmeOrderResourceSchema,
CreateAcmeAccountBodySchema,
@@ -176,5 +178,6 @@ export type TPkiAcmeServiceFactory = {
};
export type TPkiAcmeChallengeServiceFactory = {
markChallengeAsyReady: (challengeId: string) => Promise<TPkiAcmeChallenges>;
validateChallengeResponse: (challengeId: string) => Promise<void>;
};