2022-11-08 22:10:42 +08:00
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
import * as acme from "@certd/acme-client";
|
2024-10-07 03:21:16 +08:00
|
|
|
|
import { ClientExternalAccountBindingOptions, UrlMapping } from "@certd/acme-client";
|
2025-12-31 17:01:37 +08:00
|
|
|
|
import { Challenge } from "@certd/acme-client/types/rfc8555.js";
|
2024-11-06 01:17:36 +08:00
|
|
|
|
import { ILogger, utils } from "@certd/basic";
|
2026-02-07 02:20:27 +08:00
|
|
|
|
import { IContext } from "@certd/pipeline";
|
|
|
|
|
|
import { IDnsProvider, IDomainParser } from "@certd/plugin-lib";
|
2025-06-05 11:25:16 +08:00
|
|
|
|
import punycode from "punycode.js";
|
2025-12-31 17:01:37 +08:00
|
|
|
|
import { IOssClient } from "../../../plugin-lib/index.js";
|
2024-10-07 03:21:16 +08:00
|
|
|
|
export type CnameVerifyPlan = {
|
2025-01-03 00:12:15 +08:00
|
|
|
|
type?: string;
|
2024-10-07 03:21:16 +08:00
|
|
|
|
domain: string;
|
|
|
|
|
|
fullRecord: string;
|
|
|
|
|
|
dnsProvider: IDnsProvider;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-01-03 00:12:15 +08:00
|
|
|
|
export type HttpVerifyPlan = {
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
domain: string;
|
2025-04-25 01:26:04 +08:00
|
|
|
|
httpUploader: IOssClient;
|
2025-01-03 00:12:15 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-10-07 03:21:16 +08:00
|
|
|
|
export type DomainVerifyPlan = {
|
|
|
|
|
|
domain: string;
|
2025-07-13 18:25:09 +08:00
|
|
|
|
mainDomain: string;
|
2025-01-02 00:28:13 +08:00
|
|
|
|
type: "cname" | "dns" | "http";
|
2024-10-07 03:21:16 +08:00
|
|
|
|
dnsProvider?: IDnsProvider;
|
2025-07-13 18:25:09 +08:00
|
|
|
|
cnameVerifyPlan?: CnameVerifyPlan;
|
|
|
|
|
|
httpVerifyPlan?: HttpVerifyPlan;
|
2024-10-07 03:21:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
export type DomainsVerifyPlan = {
|
|
|
|
|
|
[key: string]: DomainVerifyPlan;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-01-02 00:28:13 +08:00
|
|
|
|
export type Providers = {
|
|
|
|
|
|
dnsProvider?: IDnsProvider;
|
|
|
|
|
|
domainsVerifyPlan?: DomainsVerifyPlan;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2023-05-23 18:01:20 +08:00
|
|
|
|
export type CertInfo = {
|
2024-12-12 16:45:40 +08:00
|
|
|
|
crt: string; //fullchain证书
|
|
|
|
|
|
key: string; //私钥
|
|
|
|
|
|
csr: string; //csr
|
|
|
|
|
|
oc?: string; //仅证书,非fullchain证书
|
|
|
|
|
|
ic?: string; //中间证书
|
2024-09-06 00:13:21 +08:00
|
|
|
|
pfx?: string;
|
|
|
|
|
|
der?: string;
|
2024-10-30 01:44:02 +08:00
|
|
|
|
jks?: string;
|
2024-12-17 22:45:14 +08:00
|
|
|
|
one?: string;
|
2025-08-25 18:21:38 +08:00
|
|
|
|
p7b?: string;
|
2023-05-23 18:01:20 +08:00
|
|
|
|
};
|
2025-11-11 00:32:43 +08:00
|
|
|
|
export type SSLProvider = "letsencrypt" | "google" | "zerossl" | "sslcom" | "letsencrypt_staging";
|
2024-08-25 11:56:15 +08:00
|
|
|
|
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
|
2024-07-25 10:38:45 +08:00
|
|
|
|
type AcmeServiceOptions = {
|
|
|
|
|
|
userContext: IContext;
|
2024-11-06 01:17:36 +08:00
|
|
|
|
logger: ILogger;
|
2024-07-25 10:38:45 +08:00
|
|
|
|
sslProvider: SSLProvider;
|
|
|
|
|
|
eab?: ClientExternalAccountBindingOptions;
|
|
|
|
|
|
skipLocalVerify?: boolean;
|
|
|
|
|
|
useMappingProxy?: boolean;
|
2024-10-10 15:32:25 +08:00
|
|
|
|
reverseProxy?: string;
|
2024-08-23 17:41:02 +08:00
|
|
|
|
privateKeyType?: PrivateKeyType;
|
2024-08-23 23:26:31 +08:00
|
|
|
|
signal?: AbortSignal;
|
2025-03-21 11:07:15 +08:00
|
|
|
|
maxCheckRetryCount?: number;
|
2025-04-11 12:13:57 +08:00
|
|
|
|
userId: number;
|
|
|
|
|
|
domainParser: IDomainParser;
|
2025-05-06 11:04:02 +08:00
|
|
|
|
waitDnsDiffuseTime?: number;
|
2024-07-25 10:38:45 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2022-11-08 22:10:42 +08:00
|
|
|
|
export class AcmeService {
|
2024-07-25 10:38:45 +08:00
|
|
|
|
options: AcmeServiceOptions;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
userContext: IContext;
|
2024-11-06 01:17:36 +08:00
|
|
|
|
logger: ILogger;
|
2024-07-04 01:14:09 +08:00
|
|
|
|
sslProvider: SSLProvider;
|
2024-07-08 15:35:58 +08:00
|
|
|
|
skipLocalVerify = true;
|
2024-07-04 01:14:09 +08:00
|
|
|
|
eab?: ClientExternalAccountBindingOptions;
|
2024-07-25 10:38:45 +08:00
|
|
|
|
constructor(options: AcmeServiceOptions) {
|
|
|
|
|
|
this.options = options;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
this.userContext = options.userContext;
|
|
|
|
|
|
this.logger = options.logger;
|
2024-07-04 01:14:09 +08:00
|
|
|
|
this.sslProvider = options.sslProvider || "letsencrypt";
|
|
|
|
|
|
this.eab = options.eab;
|
2024-07-08 15:35:58 +08:00
|
|
|
|
this.skipLocalVerify = options.skipLocalVerify ?? false;
|
2025-10-15 23:03:59 +08:00
|
|
|
|
// acme.setLogger((message: any, ...args: any[]) => {
|
|
|
|
|
|
// this.logger.info(message, ...args);
|
|
|
|
|
|
// });
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-23 13:15:06 +08:00
|
|
|
|
async getAccountConfig(email: string, urlMapping: UrlMapping): Promise<any> {
|
2024-08-21 10:34:50 +08:00
|
|
|
|
const conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {};
|
2024-08-23 13:15:06 +08:00
|
|
|
|
if (urlMapping && urlMapping.mappings) {
|
|
|
|
|
|
for (const key in urlMapping.mappings) {
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(urlMapping.mappings, key)) {
|
|
|
|
|
|
const element = urlMapping.mappings[key];
|
|
|
|
|
|
if (conf.accountUrl?.indexOf(element) > -1) {
|
|
|
|
|
|
//如果用了代理url,要替换回去
|
|
|
|
|
|
conf.accountUrl = conf.accountUrl.replace(element, key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-08-21 10:34:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
return conf;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buildAccountKey(email: string) {
|
2024-07-04 01:14:09 +08:00
|
|
|
|
return `acme.config.${this.sslProvider}.${email}`;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveAccountConfig(email: string, conf: any) {
|
2023-05-09 14:11:01 +08:00
|
|
|
|
await this.userContext.setObj(this.buildAccountKey(email), conf);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-11 00:32:43 +08:00
|
|
|
|
async getAcmeClient(email: string): Promise<acme.Client> {
|
2026-02-07 02:20:27 +08:00
|
|
|
|
|
|
|
|
|
|
const directoryUrl = acme.getDirectoryUrl({ sslProvider: this.sslProvider, pkType: this.options.privateKeyType });
|
|
|
|
|
|
let targetUrl = directoryUrl.replace("https://", "");
|
|
|
|
|
|
targetUrl = targetUrl.substring(0, targetUrl.indexOf("/"));
|
|
|
|
|
|
|
|
|
|
|
|
const mappings = {
|
|
|
|
|
|
"acme-v02.api.letsencrypt.org": "le.px.certd.handfree.work",
|
|
|
|
|
|
"dv.acme-v02.api.pki.goog": "gg.px.certd.handfree.work",
|
|
|
|
|
|
};
|
|
|
|
|
|
const reverseProxies = acme.getSslProviderReverseProxies();
|
|
|
|
|
|
if (reverseProxies) {
|
|
|
|
|
|
for (const key in reverseProxies) {
|
|
|
|
|
|
const value = reverseProxies[key];
|
|
|
|
|
|
if (value) {
|
|
|
|
|
|
mappings[key] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.options.reverseProxy && targetUrl) {
|
|
|
|
|
|
mappings[targetUrl] = this.options.reverseProxy;
|
2024-10-22 16:21:35 +08:00
|
|
|
|
}
|
2024-08-23 13:15:06 +08:00
|
|
|
|
const urlMapping: UrlMapping = {
|
|
|
|
|
|
enabled: false,
|
2024-10-22 16:21:35 +08:00
|
|
|
|
mappings,
|
2024-08-23 13:15:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
const conf = await this.getAccountConfig(email, urlMapping);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
if (conf.key == null) {
|
|
|
|
|
|
conf.key = await this.createNewKey();
|
|
|
|
|
|
await this.saveAccountConfig(email, conf);
|
2024-09-04 18:29:39 +08:00
|
|
|
|
this.logger.info(`创建新的Accountkey:${email}`);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
2026-02-07 02:20:27 +08:00
|
|
|
|
|
2024-07-25 10:38:45 +08:00
|
|
|
|
if (this.options.useMappingProxy) {
|
|
|
|
|
|
urlMapping.enabled = true;
|
2024-08-27 13:46:19 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
//测试directory是否可以访问
|
|
|
|
|
|
const isOk = await this.testDirectory(directoryUrl);
|
|
|
|
|
|
if (!isOk) {
|
|
|
|
|
|
this.logger.info("测试访问失败,自动使用代理");
|
|
|
|
|
|
urlMapping.enabled = true;
|
|
|
|
|
|
}
|
2024-07-25 10:38:45 +08:00
|
|
|
|
}
|
2022-11-08 22:10:42 +08:00
|
|
|
|
const client = new acme.Client({
|
2024-10-22 16:21:35 +08:00
|
|
|
|
sslProvider: this.sslProvider,
|
2024-07-04 01:14:09 +08:00
|
|
|
|
directoryUrl: directoryUrl,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
accountKey: conf.key,
|
|
|
|
|
|
accountUrl: conf.accountUrl,
|
2024-07-04 01:14:09 +08:00
|
|
|
|
externalAccountBinding: this.eab,
|
2025-03-21 11:07:15 +08:00
|
|
|
|
backoffAttempts: this.options.maxCheckRetryCount || 20,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
backoffMin: 5000,
|
2026-02-07 02:20:27 +08:00
|
|
|
|
backoffMax: 30 * 1000,
|
2024-07-25 10:38:45 +08:00
|
|
|
|
urlMapping,
|
2024-08-23 23:26:31 +08:00
|
|
|
|
signal: this.options.signal,
|
2025-10-15 23:03:59 +08:00
|
|
|
|
logger: this.logger,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (conf.accountUrl == null) {
|
|
|
|
|
|
const accountPayload = {
|
|
|
|
|
|
termsOfServiceAgreed: true,
|
|
|
|
|
|
contact: [`mailto:${email}`],
|
2024-07-04 01:14:09 +08:00
|
|
|
|
externalAccountBinding: this.eab,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
await client.createAccount(accountPayload);
|
|
|
|
|
|
conf.accountUrl = client.getAccountUrl();
|
|
|
|
|
|
await this.saveAccountConfig(email, conf);
|
|
|
|
|
|
}
|
|
|
|
|
|
return client;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async createNewKey() {
|
2024-09-04 18:29:39 +08:00
|
|
|
|
const key = await acme.crypto.createPrivateKey(2048);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
return key.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-01-03 01:17:20 +08:00
|
|
|
|
async challengeCreateFn(authz: any, keyAuthorizationGetter: (challenge: Challenge) => Promise<string>, providers: Providers) {
|
2022-11-08 22:10:42 +08:00
|
|
|
|
this.logger.info("Triggered challengeCreateFn()");
|
|
|
|
|
|
|
2024-06-14 01:22:07 +08:00
|
|
|
|
const fullDomain = authz.identifier.value;
|
2025-04-11 12:13:57 +08:00
|
|
|
|
let domain = await this.options.domainParser.parse(fullDomain);
|
2025-01-03 01:17:20 +08:00
|
|
|
|
this.logger.info("主域名为:" + domain);
|
|
|
|
|
|
|
|
|
|
|
|
const getChallenge = (type: string) => {
|
|
|
|
|
|
return authz.challenges.find((c: any) => c.type === type);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-25 01:26:04 +08:00
|
|
|
|
const doHttpVerify = async (challenge: any, httpUploader: IOssClient) => {
|
2025-01-03 01:17:20 +08:00
|
|
|
|
const keyAuthorization = await keyAuthorizationGetter(challenge);
|
|
|
|
|
|
this.logger.info("http校验");
|
2025-01-02 00:28:13 +08:00
|
|
|
|
const filePath = `.well-known/acme-challenge/${challenge.token}`;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
const fileContents = keyAuthorization;
|
2025-01-02 00:28:13 +08:00
|
|
|
|
this.logger.info(`校验 ${fullDomain} ,准备上传文件:${filePath}`);
|
2025-01-04 01:45:24 +08:00
|
|
|
|
await httpUploader.upload(filePath, Buffer.from(fileContents));
|
2025-01-02 00:28:13 +08:00
|
|
|
|
this.logger.info(`上传文件【${filePath}】成功`);
|
2025-01-03 01:17:20 +08:00
|
|
|
|
return {
|
|
|
|
|
|
challenge,
|
|
|
|
|
|
keyAuthorization,
|
2025-01-04 01:45:24 +08:00
|
|
|
|
httpUploader,
|
2025-01-03 01:17:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
2022-11-08 22:10:42 +08:00
|
|
|
|
|
2025-01-03 01:17:20 +08:00
|
|
|
|
const doDnsVerify = async (challenge: any, fullRecord: string, dnsProvider: IDnsProvider) => {
|
|
|
|
|
|
this.logger.info("dns校验");
|
|
|
|
|
|
const keyAuthorization = await keyAuthorizationGetter(challenge);
|
2024-10-07 03:21:16 +08:00
|
|
|
|
|
2025-04-24 11:55:14 +08:00
|
|
|
|
const mainDomain = dnsProvider.usePunyCode() ? domain : punycode.toUnicode(domain);
|
2025-04-25 18:04:24 +08:00
|
|
|
|
fullRecord = dnsProvider.usePunyCode() ? fullRecord : punycode.toUnicode(fullRecord);
|
2025-01-03 01:17:20 +08:00
|
|
|
|
const recordValue = keyAuthorization;
|
2025-04-24 11:55:14 +08:00
|
|
|
|
let hostRecord = fullRecord.replace(`${mainDomain}`, "");
|
2024-10-07 03:21:16 +08:00
|
|
|
|
if (hostRecord.endsWith(".")) {
|
|
|
|
|
|
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const recordReq = {
|
2025-04-24 11:55:14 +08:00
|
|
|
|
domain: mainDomain,
|
2024-10-07 03:21:16 +08:00
|
|
|
|
fullRecord,
|
|
|
|
|
|
hostRecord,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
type: "TXT",
|
|
|
|
|
|
value: recordValue,
|
2024-10-07 03:21:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
this.logger.info("添加 TXT 解析记录", JSON.stringify(recordReq));
|
|
|
|
|
|
const recordRes = await dnsProvider.createRecord(recordReq);
|
|
|
|
|
|
this.logger.info("添加 TXT 解析记录成功", JSON.stringify(recordRes));
|
|
|
|
|
|
return {
|
|
|
|
|
|
recordReq,
|
|
|
|
|
|
recordRes,
|
|
|
|
|
|
dnsProvider,
|
2025-01-03 01:17:20 +08:00
|
|
|
|
challenge,
|
|
|
|
|
|
keyAuthorization,
|
2024-10-07 03:21:16 +08:00
|
|
|
|
};
|
2025-01-03 01:17:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let dnsProvider = providers.dnsProvider;
|
|
|
|
|
|
let fullRecord = `_acme-challenge.${fullDomain}`;
|
|
|
|
|
|
|
2025-07-13 18:25:09 +08:00
|
|
|
|
// const origDomain = punycode.toUnicode(domain);
|
2025-06-05 11:25:16 +08:00
|
|
|
|
const origFullDomain = punycode.toUnicode(fullDomain);
|
2025-12-19 10:08:28 +08:00
|
|
|
|
|
|
|
|
|
|
const isIp = utils.domain.isIp(origFullDomain);
|
|
|
|
|
|
function checkIpChallenge(type: string) {
|
|
|
|
|
|
if (isIp) {
|
|
|
|
|
|
throw new Error(`IP证书不支持${type}校验方式,请选择HTTP方式校验`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-01-03 01:17:20 +08:00
|
|
|
|
if (providers.domainsVerifyPlan) {
|
|
|
|
|
|
//按照计划执行
|
2026-03-29 00:13:44 +08:00
|
|
|
|
const domainVerifyPlan = providers.domainsVerifyPlan[origFullDomain] || providers.domainsVerifyPlan[fullDomain];
|
2025-01-03 01:17:20 +08:00
|
|
|
|
if (domainVerifyPlan) {
|
|
|
|
|
|
if (domainVerifyPlan.type === "dns") {
|
2025-12-19 10:08:28 +08:00
|
|
|
|
checkIpChallenge("dns");
|
2025-01-03 01:17:20 +08:00
|
|
|
|
dnsProvider = domainVerifyPlan.dnsProvider;
|
|
|
|
|
|
} else if (domainVerifyPlan.type === "cname") {
|
2025-12-19 10:08:28 +08:00
|
|
|
|
checkIpChallenge("cname");
|
2025-07-13 18:25:09 +08:00
|
|
|
|
const cname: CnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
|
|
|
|
|
|
if (cname) {
|
|
|
|
|
|
dnsProvider = cname.dnsProvider;
|
|
|
|
|
|
domain = await this.options.domainParser.parse(cname.domain);
|
|
|
|
|
|
fullRecord = cname.fullRecord;
|
2025-01-03 01:17:20 +08:00
|
|
|
|
} else {
|
2025-05-05 21:43:39 +08:00
|
|
|
|
this.logger.error(`未找到域名${fullDomain}的CNAME校验计划,请修改证书申请配置`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (dnsProvider == null) {
|
|
|
|
|
|
throw new Error(`未找到域名${fullDomain}CNAME校验计划的DnsProvider,请修改证书申请配置`);
|
2025-01-03 01:17:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else if (domainVerifyPlan.type === "http") {
|
2025-07-13 18:25:09 +08:00
|
|
|
|
const plan: HttpVerifyPlan = domainVerifyPlan.httpVerifyPlan;
|
|
|
|
|
|
if (plan) {
|
2025-01-03 01:17:20 +08:00
|
|
|
|
const httpChallenge = getChallenge("http-01");
|
2025-01-05 01:02:41 +08:00
|
|
|
|
if (httpChallenge == null) {
|
|
|
|
|
|
throw new Error("该域名不支持http-01方式校验");
|
|
|
|
|
|
}
|
2025-01-04 01:45:24 +08:00
|
|
|
|
return await doHttpVerify(httpChallenge, plan.httpUploader);
|
2025-01-03 01:17:20 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("未找到域名【" + fullDomain + "】的http校验配置");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error("不支持的校验类型", domainVerifyPlan.type);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-07-13 23:08:00 +08:00
|
|
|
|
this.logger.warn(`未找到域名${fullDomain}的校验计划,使用默认的dnsProvider`);
|
2025-01-03 01:17:20 +08:00
|
|
|
|
}
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
2025-07-13 18:25:09 +08:00
|
|
|
|
if (!dnsProvider) {
|
2025-07-13 23:08:00 +08:00
|
|
|
|
throw new Error(`域名${fullDomain}没有匹配到任何校验方式,证书申请失败`);
|
2025-07-13 18:25:09 +08:00
|
|
|
|
}
|
2025-01-03 01:17:20 +08:00
|
|
|
|
|
|
|
|
|
|
const dnsChallenge = getChallenge("dns-01");
|
2025-12-19 10:08:28 +08:00
|
|
|
|
checkIpChallenge("dns");
|
2025-01-03 01:17:20 +08:00
|
|
|
|
return await doDnsVerify(dnsChallenge, fullRecord, dnsProvider);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Function used to remove an ACME challenge response
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {object} authz Authorization object
|
|
|
|
|
|
* @param {object} challenge Selected challenge
|
|
|
|
|
|
* @param {string} keyAuthorization Authorization key
|
2024-10-07 03:21:16 +08:00
|
|
|
|
* @param recordReq
|
|
|
|
|
|
* @param recordRes
|
2022-11-08 22:10:42 +08:00
|
|
|
|
* @param dnsProvider dnsProvider
|
2025-01-02 00:28:13 +08:00
|
|
|
|
* @param httpUploader
|
2022-11-08 22:10:42 +08:00
|
|
|
|
* @returns {Promise}
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2025-04-25 01:26:04 +08:00
|
|
|
|
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider?: IDnsProvider, httpUploader?: IOssClient) {
|
2025-01-02 00:28:13 +08:00
|
|
|
|
this.logger.info("执行清理");
|
2022-11-08 22:10:42 +08:00
|
|
|
|
|
|
|
|
|
|
/* http-01 */
|
2024-06-14 01:22:07 +08:00
|
|
|
|
const fullDomain = authz.identifier.value;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
if (challenge.type === "http-01") {
|
2025-01-02 00:28:13 +08:00
|
|
|
|
const filePath = `.well-known/acme-challenge/${challenge.token}`;
|
|
|
|
|
|
this.logger.info(`Removing challenge response for ${fullDomain} at file: ${filePath}`);
|
|
|
|
|
|
await httpUploader.remove(filePath);
|
|
|
|
|
|
this.logger.info(`删除文件【${filePath}】成功`);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
} else if (challenge.type === "dns-01") {
|
2024-10-07 03:21:16 +08:00
|
|
|
|
this.logger.info(`删除 TXT 解析记录:${JSON.stringify(recordReq)} ,recordRes = ${JSON.stringify(recordRes)}`);
|
2023-05-09 13:52:25 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await dnsProvider.removeRecord({
|
2024-10-07 03:21:16 +08:00
|
|
|
|
recordReq,
|
|
|
|
|
|
recordRes,
|
2023-05-09 13:52:25 +08:00
|
|
|
|
});
|
2024-10-07 03:21:16 +08:00
|
|
|
|
this.logger.info("删除解析记录成功");
|
2023-05-09 13:52:25 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.logger.error("删除解析记录出错:", e);
|
|
|
|
|
|
throw e;
|
|
|
|
|
|
}
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-23 17:41:02 +08:00
|
|
|
|
async order(options: {
|
|
|
|
|
|
email: string;
|
|
|
|
|
|
domains: string | string[];
|
2024-10-07 03:21:16 +08:00
|
|
|
|
dnsProvider?: any;
|
|
|
|
|
|
domainsVerifyPlan?: DomainsVerifyPlan;
|
2025-01-02 00:28:13 +08:00
|
|
|
|
httpUploader?: any;
|
2024-08-23 17:41:02 +08:00
|
|
|
|
csrInfo: any;
|
|
|
|
|
|
privateKeyType?: string;
|
2025-06-06 15:12:24 +08:00
|
|
|
|
profile?: string;
|
2025-09-06 00:41:03 +08:00
|
|
|
|
preferredChain?: string;
|
2024-08-23 17:41:02 +08:00
|
|
|
|
}): Promise<CertInfo> {
|
2025-11-11 00:32:43 +08:00
|
|
|
|
const { email, csrInfo, dnsProvider, domainsVerifyPlan, profile, preferredChain } = options;
|
|
|
|
|
|
const client: acme.Client = await this.getAcmeClient(email);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
|
2025-04-24 11:55:14 +08:00
|
|
|
|
let domains = options.domains;
|
|
|
|
|
|
const encodingDomains = [];
|
|
|
|
|
|
for (const domain of domains) {
|
|
|
|
|
|
encodingDomains.push(punycode.toASCII(domain));
|
|
|
|
|
|
}
|
|
|
|
|
|
domains = encodingDomains;
|
|
|
|
|
|
|
2022-11-08 22:10:42 +08:00
|
|
|
|
/* Create CSR */
|
2025-11-13 00:45:05 +08:00
|
|
|
|
const { altNames } = this.buildCommonNameByDomains(domains);
|
2024-08-23 17:41:02 +08:00
|
|
|
|
let privateKey = null;
|
2024-08-25 12:07:47 +08:00
|
|
|
|
const privateKeyType = options.privateKeyType || "rsa_2048";
|
|
|
|
|
|
const privateKeyArr = privateKeyType.split("_");
|
2024-08-25 11:56:15 +08:00
|
|
|
|
const type = privateKeyArr[0];
|
2024-09-06 22:32:29 +08:00
|
|
|
|
let size = 2048;
|
|
|
|
|
|
if (privateKeyArr.length > 1) {
|
|
|
|
|
|
size = parseInt(privateKeyArr[1]);
|
|
|
|
|
|
}
|
2024-09-23 14:32:57 +08:00
|
|
|
|
|
|
|
|
|
|
let encodingType = "pkcs8";
|
|
|
|
|
|
if (privateKeyArr.length > 2) {
|
|
|
|
|
|
encodingType = privateKeyArr[2];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-25 11:56:15 +08:00
|
|
|
|
if (type == "ec") {
|
|
|
|
|
|
const name: any = "P-" + size;
|
2024-09-23 14:32:57 +08:00
|
|
|
|
privateKey = await acme.crypto.createPrivateEcdsaKey(name, encodingType);
|
2024-08-23 17:41:02 +08:00
|
|
|
|
} else {
|
2024-09-23 14:32:57 +08:00
|
|
|
|
privateKey = await acme.crypto.createPrivateRsaKey(size, encodingType);
|
2024-08-23 17:41:02 +08:00
|
|
|
|
}
|
2024-09-23 14:32:57 +08:00
|
|
|
|
|
|
|
|
|
|
let createCsr: any = acme.crypto.createCsr;
|
|
|
|
|
|
if (encodingType === "pkcs1") {
|
|
|
|
|
|
//兼容老版本
|
|
|
|
|
|
createCsr = acme.forge.createCsr;
|
|
|
|
|
|
}
|
2025-11-12 23:56:02 +08:00
|
|
|
|
const csrData: any = {
|
|
|
|
|
|
// commonName,
|
|
|
|
|
|
...csrInfo,
|
|
|
|
|
|
altNames,
|
|
|
|
|
|
// emailAddress: email,
|
|
|
|
|
|
};
|
|
|
|
|
|
const [key, csr] = await createCsr(csrData, privateKey);
|
2024-09-23 14:32:57 +08:00
|
|
|
|
|
2025-04-27 01:52:42 +08:00
|
|
|
|
if (dnsProvider == null && domainsVerifyPlan == null) {
|
|
|
|
|
|
throw new Error("dnsProvider 、 domainsVerifyPlan不能都为空");
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|
2025-01-02 00:28:13 +08:00
|
|
|
|
|
|
|
|
|
|
const providers: Providers = {
|
|
|
|
|
|
dnsProvider,
|
|
|
|
|
|
domainsVerifyPlan,
|
|
|
|
|
|
};
|
2022-11-08 22:10:42 +08:00
|
|
|
|
/* 自动申请证书 */
|
|
|
|
|
|
const crt = await client.auto({
|
|
|
|
|
|
csr,
|
|
|
|
|
|
email: email,
|
|
|
|
|
|
termsOfServiceAgreed: true,
|
2024-07-08 15:35:58 +08:00
|
|
|
|
skipChallengeVerification: this.skipLocalVerify,
|
2025-01-03 00:12:15 +08:00
|
|
|
|
challengePriority: ["dns-01", "http-01"],
|
2024-10-07 03:21:16 +08:00
|
|
|
|
challengeCreateFn: async (
|
|
|
|
|
|
authz: acme.Authorization,
|
2025-01-03 01:17:20 +08:00
|
|
|
|
keyAuthorizationGetter: (challenge: Challenge) => Promise<string>
|
|
|
|
|
|
): Promise<{ recordReq?: any; recordRes?: any; dnsProvider?: any; challenge: Challenge; keyAuthorization: string }> => {
|
|
|
|
|
|
return await this.challengeCreateFn(authz, keyAuthorizationGetter, providers);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
},
|
2025-04-27 01:52:42 +08:00
|
|
|
|
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider, httpUploader: IOssClient): Promise<any> => {
|
2025-01-02 00:28:13 +08:00
|
|
|
|
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider, httpUploader);
|
2022-11-08 22:10:42 +08:00
|
|
|
|
},
|
2024-08-23 23:26:31 +08:00
|
|
|
|
signal: this.options.signal,
|
2025-06-06 15:12:24 +08:00
|
|
|
|
profile,
|
2025-09-06 00:41:03 +08:00
|
|
|
|
preferredChain,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2024-09-22 23:19:10 +08:00
|
|
|
|
const crtString = crt.toString();
|
2023-05-23 18:01:20 +08:00
|
|
|
|
const cert: CertInfo = {
|
2024-09-22 23:19:10 +08:00
|
|
|
|
crt: crtString,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
key: key.toString(),
|
|
|
|
|
|
csr: csr.toString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
/* Done */
|
|
|
|
|
|
this.logger.debug(`CSR:\n${cert.csr}`);
|
|
|
|
|
|
this.logger.debug(`Certificate:\n${cert.crt}`);
|
|
|
|
|
|
this.logger.info("证书申请成功");
|
|
|
|
|
|
return cert;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buildCommonNameByDomains(domains: string | string[]): {
|
2025-11-12 23:56:02 +08:00
|
|
|
|
commonName?: string;
|
2022-11-08 22:10:42 +08:00
|
|
|
|
altNames: string[] | undefined;
|
|
|
|
|
|
} {
|
|
|
|
|
|
if (typeof domains === "string") {
|
|
|
|
|
|
domains = domains.split(",");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (domains.length === 0) {
|
|
|
|
|
|
throw new Error("domain can not be empty");
|
|
|
|
|
|
}
|
2026-02-07 02:20:27 +08:00
|
|
|
|
|
2022-11-08 22:10:42 +08:00
|
|
|
|
return {
|
2025-11-12 23:56:02 +08:00
|
|
|
|
// commonName,
|
|
|
|
|
|
altNames: domains,
|
2022-11-08 22:10:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2024-08-27 13:46:19 +08:00
|
|
|
|
|
|
|
|
|
|
private async testDirectory(directoryUrl: string) {
|
|
|
|
|
|
try {
|
2024-09-09 17:29:09 +08:00
|
|
|
|
await utils.http.request({
|
2024-08-27 13:46:19 +08:00
|
|
|
|
url: directoryUrl,
|
|
|
|
|
|
method: "GET",
|
2024-09-04 11:26:56 +08:00
|
|
|
|
timeout: 10000,
|
2024-08-27 13:46:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
2024-10-25 10:57:38 +08:00
|
|
|
|
this.logger.error(`${directoryUrl},测试访问失败`, e.message);
|
2024-08-27 13:46:19 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.logger.info(`${directoryUrl},测试访问成功`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2022-11-08 22:10:42 +08:00
|
|
|
|
}
|