mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 21:28:18 +00:00
Add mark as ready
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { isPrivateIp } from "@app/lib/ip/ipRange";
|
import { isPrivateIp } from "@app/lib/ip/ipRange";
|
||||||
@@ -18,7 +19,11 @@ import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
|
|||||||
type TPkiAcmeChallengeServiceFactoryDep = {
|
type TPkiAcmeChallengeServiceFactoryDep = {
|
||||||
acmeChallengeDAL: Pick<
|
acmeChallengeDAL: Pick<
|
||||||
TPkiAcmeChallengeDALFactory,
|
TPkiAcmeChallengeDALFactory,
|
||||||
"transaction" | "findByIdForChallengeValidation" | "markAsValidCascadeById" | "markAsInvalidCascadeById"
|
| "transaction"
|
||||||
|
| "findByIdForChallengeValidation"
|
||||||
|
| "markAsValidCascadeById"
|
||||||
|
| "markAsInvalidCascadeById"
|
||||||
|
| "updateById"
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,9 +31,8 @@ export const pkiAcmeChallengeServiceFactory = ({
|
|||||||
acmeChallengeDAL
|
acmeChallengeDAL
|
||||||
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
|
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
const markChallengeAsyReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => {
|
||||||
const validateChallengeResponse = async (challengeId: string): Promise<void> => {
|
return acmeChallengeDAL.transaction(async (tx) => {
|
||||||
const error: Error | undefined = await acmeChallengeDAL.transaction(async (tx) => {
|
|
||||||
logger.info({ challengeId }, "Validating ACME challenge response");
|
logger.info({ challengeId }, "Validating ACME challenge response");
|
||||||
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
|
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
@@ -52,81 +56,92 @@ export const pkiAcmeChallengeServiceFactory = ({
|
|||||||
if (challenge.type !== AcmeChallengeType.HTTP_01) {
|
if (challenge.type !== AcmeChallengeType.HTTP_01) {
|
||||||
throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" });
|
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
|
// check if host is a private ip address
|
||||||
if (isPrivateIp(host)) {
|
if (isPrivateIp(host)) {
|
||||||
throw new BadRequestError({ message: "Private IP addresses are not allowed" });
|
throw new BadRequestError({ message: "Private IP addresses are not allowed" });
|
||||||
}
|
}
|
||||||
if (appCfg.isAcmeDevelopmentMode && appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host]) {
|
return acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Processing }, tx);
|
||||||
host = appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES[host];
|
|
||||||
logger.warn(
|
|
||||||
{ srcHost: challenge.auth.identifierValue, dstHost: host },
|
|
||||||
"Using ACME development HTTP-01 challenge host override"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, `http://${host}`);
|
|
||||||
logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation");
|
|
||||||
try {
|
|
||||||
// TODO: read config from the profile to get the timeout instead
|
|
||||||
const timeoutMs = 10 * 1000; // 10 seconds
|
|
||||||
// Notice: well, we are in a transaction, ideally we should not hold transaction and perform
|
|
||||||
// a long running operation for long time. But assuming we are not performing a tons of
|
|
||||||
// challenge validation at the same time, it should be fine.
|
|
||||||
const challengeResponse = await axios.get<string>(challengeUrl.toString(), {
|
|
||||||
// In case if we override the host in the development mode, still provide the original host in the header
|
|
||||||
// to help the upstream server to validate the request
|
|
||||||
headers: { Host: challenge.auth.identifierValue },
|
|
||||||
timeout: timeoutMs,
|
|
||||||
responseType: "text",
|
|
||||||
validateStatus: () => true
|
|
||||||
});
|
|
||||||
if (challengeResponse.status !== 200) {
|
|
||||||
throw new AcmeIncorrectResponseError({
|
|
||||||
message: `ACME challenge response is not 200: ${challengeResponse.status}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const challengeResponseBody: string = challengeResponse.data;
|
|
||||||
const thumbprint = challenge.auth.account.publicKeyThumbprint;
|
|
||||||
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
|
|
||||||
if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) {
|
|
||||||
throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" });
|
|
||||||
}
|
|
||||||
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx);
|
|
||||||
} 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);
|
|
||||||
// Properly type and inspect the error
|
|
||||||
if (axios.isAxiosError(exp)) {
|
|
||||||
const axiosError = exp as AxiosError;
|
|
||||||
const errorCode = axiosError.code;
|
|
||||||
const errorMessage = axiosError.message;
|
|
||||||
|
|
||||||
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
|
|
||||||
return new AcmeConnectionError({ message: "Connection refused" });
|
|
||||||
}
|
|
||||||
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
|
|
||||||
return 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" });
|
|
||||||
}
|
|
||||||
logger.error(exp, "Unknown error validating ACME challenge response");
|
|
||||||
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
|
|
||||||
}
|
|
||||||
if (exp instanceof Error) {
|
|
||||||
logger.error(exp, "Error validating ACME challenge response");
|
|
||||||
} else {
|
|
||||||
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;
|
|
||||||
|
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(
|
||||||
|
{ srcHost: challenge.auth.identifierValue, dstHost: host },
|
||||||
|
"Using ACME development HTTP-01 challenge host override"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, `http://${host}`);
|
||||||
|
logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation");
|
||||||
|
try {
|
||||||
|
// TODO: read config from the profile to get the timeout instead
|
||||||
|
const timeoutMs = 10 * 1000; // 10 seconds
|
||||||
|
// Notice: well, we are in a transaction, ideally we should not hold transaction and perform
|
||||||
|
// a long running operation for long time. But assuming we are not performing a tons of
|
||||||
|
// challenge validation at the same time, it should be fine.
|
||||||
|
const challengeResponse = await axios.get<string>(challengeUrl.toString(), {
|
||||||
|
// In case if we override the host in the development mode, still provide the original host in the header
|
||||||
|
// to help the upstream server to validate the request
|
||||||
|
headers: { Host: challenge.auth.identifierValue },
|
||||||
|
timeout: timeoutMs,
|
||||||
|
responseType: "text",
|
||||||
|
validateStatus: () => true
|
||||||
|
});
|
||||||
|
if (challengeResponse.status !== 200) {
|
||||||
|
throw new AcmeIncorrectResponseError({
|
||||||
|
message: `ACME challenge response is not 200: ${challengeResponse.status}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const challengeResponseBody: string = challengeResponse.data;
|
||||||
|
const thumbprint = challenge.auth.account.publicKeyThumbprint;
|
||||||
|
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
|
||||||
|
if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) {
|
||||||
|
throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" });
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
// Properly type and inspect the error
|
||||||
|
if (axios.isAxiosError(exp)) {
|
||||||
|
const axiosError = exp as AxiosError;
|
||||||
|
const errorCode = axiosError.code;
|
||||||
|
const errorMessage = axiosError.message;
|
||||||
|
|
||||||
|
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
|
||||||
|
throw new AcmeConnectionError({ message: "Connection refused" });
|
||||||
|
}
|
||||||
|
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
|
||||||
|
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");
|
||||||
|
throw new AcmeConnectionError({ message: "Connection timed out" });
|
||||||
|
}
|
||||||
|
logger.error(exp, "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");
|
||||||
|
throw exp;
|
||||||
|
}
|
||||||
|
logger.error(exp, "Unknown error validating ACME challenge response");
|
||||||
|
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { validateChallengeResponse };
|
return { markChallengeAsyReady, validateChallengeResponse };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { JWSHeaderParameters } from "jose";
|
import { JWSHeaderParameters } from "jose";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TPkiAcmeChallenges } from "@app/db/schemas/pki-acme-challenges";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AcmeOrderResourceSchema,
|
AcmeOrderResourceSchema,
|
||||||
CreateAcmeAccountBodySchema,
|
CreateAcmeAccountBodySchema,
|
||||||
@@ -176,5 +178,6 @@ export type TPkiAcmeServiceFactory = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TPkiAcmeChallengeServiceFactory = {
|
export type TPkiAcmeChallengeServiceFactory = {
|
||||||
|
markChallengeAsyReady: (challengeId: string) => Promise<TPkiAcmeChallenges>;
|
||||||
validateChallengeResponse: (challengeId: string) => Promise<void>;
|
validateChallengeResponse: (challengeId: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user