Files
certd/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts
2024-10-10 03:08:31 +08:00

313 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from "@certd/pipeline";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import _ from "lodash-es";
import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js";
import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
export type { CertInfo };
export * from "./cert-reader.js";
export type CnameRecordInput = {
id: number;
status: string;
};
export type DomainVerifyPlanInput = {
domain: string;
type: "cname" | "dns";
dnsProviderType?: string;
dnsProviderAccessId?: number;
cnameVerifyPlan?: Record<string, CnameRecordInput>;
};
export type DomainsVerifyPlanInput = {
[key: string]: DomainVerifyPlanInput;
};
@IsTaskPlugin({
name: "CertApply",
title: "证书申请JS版",
icon: "ph:certificate",
group: pluginGroups.cert.key,
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
default: {
input: {
renewDays: 20,
forceUpdate: false,
},
strategy: {
runStrategy: RunStrategy.AlwaysRun,
},
},
})
export class CertApplyPlugin extends CertApplyBasePlugin {
@TaskInput({
title: "域名验证方式",
value: "dns",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "dns", label: "DNS直接验证" },
{ value: "cname", label: "CNAME间接验证" },
],
},
required: true,
helper:
"DNS直接验证适合域名是在阿里云、腾讯云、华为云、Cloudflare、西数注册的需要提供Access授权信息。\nCNAME间接验证支持任何注册商注册的域名并且不需要提供Access授权信息但第一次需要手动添加CNAME记录",
})
challengeType!: string;
@TaskInput({
title: "DNS提供商",
component: {
name: "dns-provider-selector",
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.challengeType === 'dns'
})
}
`,
required: true,
helper: "请选择dns解析提供商您的域名是在哪里注册的或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商请选择CNAME间接验证校验方式",
})
dnsProviderType!: string;
@TaskInput({
title: "DNS解析授权",
component: {
name: "access-selector",
},
required: true,
helper: "请选择dns解析提供商授权",
mergeScript: `return {
component:{
type: ctx.compute(({form})=>{
return form.dnsProviderType
})
},
show: ctx.compute(({form})=>{
return form.challengeType === 'dns'
})
}
`,
})
dnsProviderAccess!: number;
@TaskInput({
title: "域名验证配置",
component: {
name: "domains-verify-plan-editor",
},
rules: [{ type: "checkCnameVerifyPlan" }],
required: true,
helper: "如果选择CNAME方式请按照上面的显示给域名添加CNAME记录添加后点击验证验证成功后不要删除记录申请和续期证书会一直用它",
col: {
span: 24,
},
mergeScript: `return {
component:{
domains: ctx.compute(({form})=>{
return form.domains
})
},
show: ctx.compute(({form})=>{
return form.challengeType === 'cname'
})
}
`,
})
domainsVerifyPlan!: DomainsVerifyPlanInput;
@TaskInput({
title: "证书提供商",
value: "letsencrypt",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "google", label: "Google" },
{ value: "zerossl", label: "ZeroSSL" },
],
},
helper: "Let's Encrypt最简单如果使用ZeroSSL、Google证书需要提供EAB授权",
required: true,
})
sslProvider!: SSLProvider;
@TaskInput({
title: "EAB授权",
component: {
name: "access-selector",
type: "eab",
},
maybeNeed: true,
required: true,
helper:
"需要提供EAB授权\nZeroSSL请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials' \n Google请查看[google获取eab帮助文档](https://github.com/certd/certd/blob/v2/doc/google/google.md)",
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.sslProvider === 'zerossl' || form.sslProvider === 'google'
})
}
`,
})
eabAccessId!: number;
@TaskInput({
title: "加密算法",
value: "rsa_2048",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "rsa_1024", label: "RSA 1024" },
{ value: "rsa_2048", label: "RSA 2048" },
{ value: "rsa_3072", label: "RSA 3072" },
{ value: "rsa_4096", label: "RSA 4096" },
{ value: "rsa_2048_pkcs1", label: "RSA 2048 pkcs1 (旧版)" },
{ value: "ec_256", label: "EC 256" },
{ value: "ec_384", label: "EC 384" },
// { value: "ec_521", label: "EC 521" },
],
},
helper: "如无特殊需求,默认即可",
required: true,
})
privateKeyType!: PrivateKeyType;
@TaskInput({
title: "使用代理",
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问请尝试开启此选项\n默认情况会进行测试如果无法访问将会自动使用代理",
})
useProxy = false;
@TaskInput({
title: "跳过本地校验DNS",
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "跳过本地校验可以加快申请速度,同时也会增加失败概率。",
})
skipLocalVerify = false;
acme!: AcmeService;
async onInit() {
let eab: any = null;
if (this.eabAccessId) {
eab = await this.ctx.accessService.getById(this.eabAccessId);
}
this.acme = new AcmeService({
userContext: this.userContext,
logger: this.logger,
sslProvider: this.sslProvider,
eab,
skipLocalVerify: this.skipLocalVerify,
useMappingProxy: this.useProxy,
privateKeyType: this.privateKeyType,
// cnameProxyService: this.ctx.cnameProxyService,
// dnsProviderCreator: this.createDnsProvider.bind(this),
});
}
async doCertApply() {
const email = this["email"];
const domains = this["domains"];
const csrInfo = _.merge(
{
country: "CN",
state: "GuangDong",
locality: "ShengZhen",
organization: "CertD Org.",
organizationUnit: "IT Department",
emailAddress: email,
},
this.csrInfo ? JSON.parse(this.csrInfo) : {}
);
this.logger.info("开始申请证书,", email, domains);
let dnsProvider: any = null;
let domainsVerifyPlan: DomainsVerifyPlan = null;
if (this.challengeType === "cname") {
domainsVerifyPlan = await this.createDomainsVerifyPlan();
} else {
const dnsProviderType = this.dnsProviderType;
const dnsProviderAccessId = this.dnsProviderAccess;
dnsProvider = await this.createDnsProvider(dnsProviderType, dnsProviderAccessId);
}
try {
const cert = await this.acme.order({
email,
domains,
dnsProvider,
domainsVerifyPlan,
csrInfo,
isTest: false,
privateKeyType: this.privateKeyType,
});
const certInfo = this.formatCerts(cert);
return new CertReader(certInfo);
} catch (e: any) {
const message: string = e?.message;
if (message != null && message.indexOf("redundant with a wildcard domain in the same request") >= 0) {
this.logger.error(e);
throw new Error(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
}
throw e;
}
}
async createDnsProvider(dnsProviderType: string, dnsProviderAccessId: number): Promise<IDnsProvider> {
const access = await this.accessService.getById(dnsProviderAccessId);
const context: DnsProviderContext = { access, logger: this.logger, http: this.ctx.http, utils };
return await createDnsProvider({
dnsProviderType,
context,
});
}
async createDomainsVerifyPlan(): Promise<DomainsVerifyPlan> {
const plan: DomainsVerifyPlan = {};
for (const domain in this.domainsVerifyPlan) {
const domainVerifyPlan = this.domainsVerifyPlan[domain];
let dnsProvider = null;
const cnameVerifyPlan: Record<string, CnameVerifyPlan> = {};
if (domainVerifyPlan.type === "dns") {
dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, domainVerifyPlan.dnsProviderAccessId);
} else {
for (const key in domainVerifyPlan.cnameVerifyPlan) {
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key);
cnameVerifyPlan[key] = {
domain: cnameRecord.cnameProvider.domain,
fullRecord: cnameRecord.recordValue,
dnsProvider: await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.accessId),
};
}
}
plan[domain] = {
domain,
type: domainVerifyPlan.type,
dnsProvider,
cnameVerifyPlan,
};
}
return plan;
}
}
new CertApplyPlugin();