mirror of
https://github.com/certd/certd.git
synced 2026-04-03 14:10:54 +08:00
chore: 修复metadata的一些bug
This commit is contained in:
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
| 序号 | 名称 | 说明 |
|
| 序号 | 名称 | 说明 |
|
||||||
|-----|-----|-----|
|
|-----|-----|-----|
|
||||||
| 1.| **商用证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) |
|
| 1.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
||||||
| 2.| **获取阿里云订阅证书** | 从阿里云拉取订阅模式的商用证书 |
|
| 2.| **商用证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) |
|
||||||
| 3.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
| 3.| **获取阿里云订阅证书** | 从阿里云拉取订阅模式的商用证书 |
|
||||||
| 4.| **证书申请(Lego)** | 支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
| 4.| **证书申请(Lego)** | 支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
||||||
## 2. 主机
|
## 2. 主机
|
||||||
|
|
||||||
|
|||||||
@@ -992,7 +992,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const { viewCert, downloadCert } = useCertViewer();
|
const { viewCert, downloadCert } = useCertViewer();
|
||||||
const isCert = computed(() => {
|
const isCert = computed(() => {
|
||||||
return currentPipeline.value?.type?.startsWith("cert");
|
return currentPipeline.value?.type?.startsWith("cert") || pipelineDetail.value.lastVars?.certExpiresTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasWebhookTrigger = computed(() => {
|
const hasWebhookTrigger = computed(() => {
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ async function genMetadata(){
|
|||||||
for (const key in modules) {
|
for (const key in modules) {
|
||||||
const module = modules[key]
|
const module = modules[key]
|
||||||
const entry = Object.entries(module)
|
const entry = Object.entries(module)
|
||||||
|
if (entry.length >1) {
|
||||||
|
console.log(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`)
|
||||||
|
}
|
||||||
for (const [name, value] of entry) {
|
for (const [name, value] of entry) {
|
||||||
//如果有define属性
|
//如果有define属性
|
||||||
if(value.define){
|
if(value.define){
|
||||||
|
|||||||
@@ -440,4 +440,4 @@ output:
|
|||||||
type: certZip
|
type: certZip
|
||||||
pluginType: deploy
|
pluginType: deploy
|
||||||
type: builtIn
|
type: builtIn
|
||||||
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/index.js
|
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/apply.js
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"lint:fix": "mwts fix",
|
"lint:fix": "mwts fix",
|
||||||
"ci": "npm run cov",
|
"ci": "npm run cov",
|
||||||
"build-only": "cross-env NODE_ENV=production mwtsc --cleanOutDir --skipLibCheck",
|
"build-only": "cross-env NODE_ENV=production mwtsc --cleanOutDir --skipLibCheck",
|
||||||
"build": "npm run build_only && npm run export-metadata",
|
"build": "npm run build-only && npm run export-metadata",
|
||||||
"export-metadata": "node export-plugin-yaml.js",
|
"export-metadata": "node export-plugin-yaml.js",
|
||||||
"export-metadata-only": "node export-plugin-yaml.js docoff",
|
"export-metadata-only": "node export-plugin-yaml.js docoff",
|
||||||
"dev-build": "echo 1",
|
"dev-build": "echo 1",
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
|
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
|
||||||
|
|
||||||
export const AwsCNRegions = [
|
|
||||||
{ label: 'cn-north-1', value: 'cn-north-1' },
|
|
||||||
{ label: 'cn-northwest-1', value: 'cn-northwest-1' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@IsAccess({
|
@IsAccess({
|
||||||
name: 'aws-cn',
|
name: 'aws-cn',
|
||||||
title: '亚马逊云科技(国区)授权',
|
title: '亚马逊云科技(国区)授权',
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const AwsCNRegions = [
|
||||||
|
{ label: 'cn-north-1', value: 'cn-north-1' },
|
||||||
|
{ label: 'cn-northwest-1', value: 'cn-northwest-1' },
|
||||||
|
];
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './plugins/index.js';
|
export * from './plugins/index.js';
|
||||||
export * from './access.js';
|
export * from './access.js';
|
||||||
|
export * from './constants.js';
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||||
import { AwsCNAccess, AwsCNRegions } from "../access.js";
|
import { AwsCNAccess } from "../access.js";
|
||||||
import { AwsIAMClient } from "../libs/aws-iam-client.js";
|
import { AwsIAMClient } from "../libs/aws-iam-client.js";
|
||||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||||
|
import { AwsCNRegions } from '../constants.js';
|
||||||
|
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: 'AwsCNDeployToCloudFront',
|
name: 'AwsCNDeployToCloudFront',
|
||||||
|
|||||||
@@ -1,41 +1,5 @@
|
|||||||
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
|
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
|
||||||
|
import { AwsRegions } from './constants.js';
|
||||||
export const AwsRegions = [
|
|
||||||
{ label: '------中国区------', value: 'cn',disabled: true },
|
|
||||||
{ label: '北京', value: 'cn-north-1' },
|
|
||||||
{ label: '宁夏', value: 'cn-northwest-1' },
|
|
||||||
{ label: '------海外-----', value: 'out',disabled: true },
|
|
||||||
{ label: 'us-east-1', value: 'us-east-1' },
|
|
||||||
{ label: 'us-east-2', value: 'us-east-2' },
|
|
||||||
{ label: 'us-west-1', value: 'us-west-1' },
|
|
||||||
{ label: 'us-west-2', value: 'us-west-2' },
|
|
||||||
{ label: 'af-south-1', value: 'af-south-1' },
|
|
||||||
{ label: 'ap-east-1', value: 'ap-east-1' },
|
|
||||||
{ label: 'ap-northeast-1', value: 'ap-northeast-1' },
|
|
||||||
{ label: 'ap-northeast-2', value: 'ap-northeast-2' },
|
|
||||||
{ label: 'ap-northeast-3', value: 'ap-northeast-3' },
|
|
||||||
{ label: 'ap-south-1', value: 'ap-south-1' },
|
|
||||||
{ label: 'ap-south-2', value: 'ap-south-2' },
|
|
||||||
{ label: 'ap-southeast-1', value: 'ap-southeast-1' },
|
|
||||||
{ label: 'ap-southeast-2', value: 'ap-southeast-2' },
|
|
||||||
{ label: 'ap-southeast-3', value: 'ap-southeast-3' },
|
|
||||||
{ label: 'ap-southeast-4', value: 'ap-southeast-4' },
|
|
||||||
{ label: 'ap-southeast-5', value: 'ap-southeast-5' },
|
|
||||||
{ label: 'ca-central-1', value: 'ca-central-1' },
|
|
||||||
{ label: 'ca-west-1', value: 'ca-west-1' },
|
|
||||||
{ label: 'eu-central-1', value: 'eu-central-1' },
|
|
||||||
{ label: 'eu-central-2', value: 'eu-central-2' },
|
|
||||||
{ label: 'eu-north-1', value: 'eu-north-1' },
|
|
||||||
{ label: 'eu-south-1', value: 'eu-south-1' },
|
|
||||||
{ label: 'eu-south-2', value: 'eu-south-2' },
|
|
||||||
{ label: 'eu-west-1', value: 'eu-west-1' },
|
|
||||||
{ label: 'eu-west-2', value: 'eu-west-2' },
|
|
||||||
{ label: 'eu-west-3', value: 'eu-west-3' },
|
|
||||||
{ label: 'il-central-1', value: 'il-central-1' },
|
|
||||||
{ label: 'me-central-1', value: 'me-central-1' },
|
|
||||||
{ label: 'me-south-1', value: 'me-south-1' },
|
|
||||||
{ label: 'sa-east-1', value: 'sa-east-1' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@IsAccess({
|
@IsAccess({
|
||||||
name: 'aws',
|
name: 'aws',
|
||||||
|
|||||||
37
packages/ui/certd-server/src/plugins/plugin-aws/constants.ts
Normal file
37
packages/ui/certd-server/src/plugins/plugin-aws/constants.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
export const AwsRegions = [
|
||||||
|
{ label: '------中国区------', value: 'cn',disabled: true },
|
||||||
|
{ label: '北京', value: 'cn-north-1' },
|
||||||
|
{ label: '宁夏', value: 'cn-northwest-1' },
|
||||||
|
{ label: '------海外-----', value: 'out',disabled: true },
|
||||||
|
{ label: 'us-east-1', value: 'us-east-1' },
|
||||||
|
{ label: 'us-east-2', value: 'us-east-2' },
|
||||||
|
{ label: 'us-west-1', value: 'us-west-1' },
|
||||||
|
{ label: 'us-west-2', value: 'us-west-2' },
|
||||||
|
{ label: 'af-south-1', value: 'af-south-1' },
|
||||||
|
{ label: 'ap-east-1', value: 'ap-east-1' },
|
||||||
|
{ label: 'ap-northeast-1', value: 'ap-northeast-1' },
|
||||||
|
{ label: 'ap-northeast-2', value: 'ap-northeast-2' },
|
||||||
|
{ label: 'ap-northeast-3', value: 'ap-northeast-3' },
|
||||||
|
{ label: 'ap-south-1', value: 'ap-south-1' },
|
||||||
|
{ label: 'ap-south-2', value: 'ap-south-2' },
|
||||||
|
{ label: 'ap-southeast-1', value: 'ap-southeast-1' },
|
||||||
|
{ label: 'ap-southeast-2', value: 'ap-southeast-2' },
|
||||||
|
{ label: 'ap-southeast-3', value: 'ap-southeast-3' },
|
||||||
|
{ label: 'ap-southeast-4', value: 'ap-southeast-4' },
|
||||||
|
{ label: 'ap-southeast-5', value: 'ap-southeast-5' },
|
||||||
|
{ label: 'ca-central-1', value: 'ca-central-1' },
|
||||||
|
{ label: 'ca-west-1', value: 'ca-west-1' },
|
||||||
|
{ label: 'eu-central-1', value: 'eu-central-1' },
|
||||||
|
{ label: 'eu-central-2', value: 'eu-central-2' },
|
||||||
|
{ label: 'eu-north-1', value: 'eu-north-1' },
|
||||||
|
{ label: 'eu-south-1', value: 'eu-south-1' },
|
||||||
|
{ label: 'eu-south-2', value: 'eu-south-2' },
|
||||||
|
{ label: 'eu-west-1', value: 'eu-west-1' },
|
||||||
|
{ label: 'eu-west-2', value: 'eu-west-2' },
|
||||||
|
{ label: 'eu-west-3', value: 'eu-west-3' },
|
||||||
|
{ label: 'il-central-1', value: 'il-central-1' },
|
||||||
|
{ label: 'me-central-1', value: 'me-central-1' },
|
||||||
|
{ label: 'me-south-1', value: 'me-south-1' },
|
||||||
|
{ label: 'sa-east-1', value: 'sa-east-1' },
|
||||||
|
];
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './plugins/index.js';
|
export * from './plugins/index.js';
|
||||||
export * from './access.js';
|
export * from './access.js';
|
||||||
export * from './aws-route53-provider.js';
|
export * from './aws-route53-provider.js';
|
||||||
|
export * from './constants.js';
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||||
import { AwsAccess, AwsRegions } from "../access.js";
|
import { AwsAccess } from "../access.js";
|
||||||
import { AwsClient } from "../libs/aws-client.js";
|
import { AwsClient } from "../libs/aws-client.js";
|
||||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||||
import { optionsUtils } from "@certd/basic";
|
import { optionsUtils } from "@certd/basic";
|
||||||
|
import { AwsRegions } from "../constants.js";
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: 'AwsDeployToCloudFront',
|
name: 'AwsDeployToCloudFront',
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
|
||||||
import { CertInfo } from '@certd/plugin-cert';
|
import { CertInfo } from '@certd/plugin-cert';
|
||||||
import { AwsAccess, AwsRegions } from '../access.js';
|
import { AwsAccess } from '../access.js';
|
||||||
import { AwsClient } from '../libs/aws-client.js';
|
import { AwsClient } from '../libs/aws-client.js';
|
||||||
import { CertApplyPluginNames} from '@certd/plugin-cert';
|
import { CertApplyPluginNames} from '@certd/plugin-cert';
|
||||||
|
import { AwsRegions } from "../constants.js";
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: 'AwsUploadToACM',
|
name: 'AwsUploadToACM',
|
||||||
title: 'AWS-上传证书到ACM',
|
title: 'AWS-上传证书到ACM',
|
||||||
|
|||||||
@@ -0,0 +1,667 @@
|
|||||||
|
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
|
import { utils } from "@certd/basic";
|
||||||
|
|
||||||
|
import { AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
|
||||||
|
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib";
|
||||||
|
import { CertReader } from "@certd/plugin-lib";
|
||||||
|
import { CertApplyBasePlugin } from "./base.js";
|
||||||
|
import { GoogleClient } from "../../libs/google.js";
|
||||||
|
import { EabAccess } from "../../access/index.js";
|
||||||
|
import { DomainParser } from "@certd/plugin-lib";
|
||||||
|
import { ossClientFactory } from "../../../plugin-lib/oss/factory.js";
|
||||||
|
import { merge } from "lodash-es";
|
||||||
|
|
||||||
|
export type CnameRecordInput = {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HttpRecordInput = {
|
||||||
|
domain: string;
|
||||||
|
httpUploaderType: string;
|
||||||
|
httpUploaderAccess: number;
|
||||||
|
httpUploadRootDir: string;
|
||||||
|
};
|
||||||
|
export type DomainVerifyPlanInput = {
|
||||||
|
domain: string;
|
||||||
|
type: "cname" | "dns" | "http";
|
||||||
|
dnsProviderType?: string;
|
||||||
|
dnsProviderAccessType?: string;
|
||||||
|
dnsProviderAccessId?: number;
|
||||||
|
cnameVerifyPlan?: Record<string, CnameRecordInput>;
|
||||||
|
httpVerifyPlan?: Record<string, HttpRecordInput>;
|
||||||
|
};
|
||||||
|
export type DomainsVerifyPlanInput = {
|
||||||
|
[key: string]: DomainVerifyPlanInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
const preferredChainConfigs = {
|
||||||
|
letsencrypt: {
|
||||||
|
helper: "如无特殊需求保持默认即可",
|
||||||
|
options: [
|
||||||
|
{ value: "ISRG Root X1", label: "ISRG Root X1" },
|
||||||
|
{ value: "ISRG Root X2", label: "ISRG Root X2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
helper: "GlobalSign 提供对老旧设备更好的兼容性,但证书链会变长",
|
||||||
|
options: [
|
||||||
|
{ value: "GTS Root R1", label: "GTS Root R1" },
|
||||||
|
{ value: "GlobalSign", label: "GlobalSign" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
|
||||||
|
|
||||||
|
const preferredChainMergeScript = (() => {
|
||||||
|
const configs = JSON.stringify(preferredChainConfigs);
|
||||||
|
const supportedProviders = JSON.stringify(preferredChainSupportedProviders);
|
||||||
|
const defaultProvider = JSON.stringify(preferredChainSupportedProviders[0]);
|
||||||
|
return `
|
||||||
|
const chainConfigs = ${configs};
|
||||||
|
const supportedProviders = ${supportedProviders};
|
||||||
|
const defaultProvider = ${defaultProvider};
|
||||||
|
const getConfig = (provider)=> chainConfigs[provider] || chainConfigs[defaultProvider];
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=> supportedProviders.includes(form.sslProvider)),
|
||||||
|
component: {
|
||||||
|
options: ctx.compute(({form})=> getConfig(form.sslProvider).options)
|
||||||
|
},
|
||||||
|
helper: ctx.compute(({form})=> getConfig(form.sslProvider).helper),
|
||||||
|
value: ctx.compute(({form})=>{
|
||||||
|
const { options } = getConfig(form.sslProvider);
|
||||||
|
const allowed = options.map(item=>item.value);
|
||||||
|
const current = form.preferredChain;
|
||||||
|
if(allowed.includes(current)){
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return allowed[0];
|
||||||
|
})
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
@IsTaskPlugin({
|
||||||
|
name: "CertApply",
|
||||||
|
title: "证书申请(JS版)",
|
||||||
|
icon: "ph:certificate",
|
||||||
|
group: pluginGroups.cert.key,
|
||||||
|
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
|
||||||
|
default: {
|
||||||
|
input: {
|
||||||
|
renewDays: 18,
|
||||||
|
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代理验证" },
|
||||||
|
{ value: "http", label: "HTTP文件验证(IP证书只能选它)" },
|
||||||
|
{ value: "dnses", label: "多DNS提供商" },
|
||||||
|
{ value: "auto", label: "自动匹配" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
helper: `1. <b>DNS直接验证</b>:当域名dns解析已被本系统支持时(即下方DNS解析服务商选项中可选),推荐选择此方式
|
||||||
|
2. <b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(如果经常申请失败,建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证)
|
||||||
|
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
|
||||||
|
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
|
||||||
|
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
challengeType!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "证书颁发机构",
|
||||||
|
value: "letsencrypt",
|
||||||
|
component: {
|
||||||
|
name: "icon-select",
|
||||||
|
vModel: "value",
|
||||||
|
options: [
|
||||||
|
{ value: "letsencrypt", label: "Let's Encrypt(免费,新手推荐,支持IP证书)", icon: "simple-icons:letsencrypt" },
|
||||||
|
{ value: "google", label: "Google(免费)", icon: "flat-color-icons:google" },
|
||||||
|
{ value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" },
|
||||||
|
{ value: "litessl", label: "litessl(免费)", icon: "roentgen:free" },
|
||||||
|
{ value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" },
|
||||||
|
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境(仅供测试)", icon: "simple-icons:letsencrypt" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,仅首次需要翻墙获取EAB授权\nZeroSSL:需要EAB授权,无需翻墙\nSSL.com:仅主域名和www免费,必须设置CAA记录",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
sslProvider!: SSLProvider;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "DNS解析服务商",
|
||||||
|
component: {
|
||||||
|
name: "dns-provider-selector",
|
||||||
|
},
|
||||||
|
mergeScript: `
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType === 'dns'
|
||||||
|
}),
|
||||||
|
component:{
|
||||||
|
onSelectedChange: ctx.compute(({form})=>{
|
||||||
|
return ($event)=>{
|
||||||
|
form.dnsProviderAccessType = $event.accessType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
required: true,
|
||||||
|
helper: "您的域名注册商,或者域名的dns服务器属于哪个平台\n如果这里没有,请选择CNAME代理验证校验方式",
|
||||||
|
})
|
||||||
|
dnsProviderType!: string;
|
||||||
|
|
||||||
|
// dns解析授权类型,勿删
|
||||||
|
dnsProviderAccessType!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "DNS解析授权",
|
||||||
|
component: {
|
||||||
|
name: "access-selector",
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
helper: "请选择dns解析服务商授权",
|
||||||
|
mergeScript: `return {
|
||||||
|
component:{
|
||||||
|
type: ctx.compute(({form})=>{
|
||||||
|
return form.dnsProviderAccessType || form.dnsProviderType
|
||||||
|
})
|
||||||
|
},
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType === 'dns'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
dnsProviderAccess!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "域名验证配置",
|
||||||
|
component: {
|
||||||
|
name: "domains-verify-plan-editor",
|
||||||
|
},
|
||||||
|
rules: [{ type: "checkDomainVerifyPlan" }],
|
||||||
|
required: true,
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
mergeScript: `return {
|
||||||
|
component:{
|
||||||
|
domains: ctx.compute(({form})=>{
|
||||||
|
return form.domains
|
||||||
|
}),
|
||||||
|
defaultType: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType || 'cname'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.challengeType === 'cname' || form.challengeType === 'http' || form.challengeType === 'dnses'
|
||||||
|
}),
|
||||||
|
helper: ctx.compute(({form})=>{
|
||||||
|
if(form.challengeType === 'cname' ){
|
||||||
|
return '请按照上面的提示,给要申请证书的域名添加CNAME记录,添加后,点击验证,验证成功后不要删除记录,申请和续期证书会一直用它'
|
||||||
|
}else if (form.challengeType === 'http'){
|
||||||
|
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录的.well-known/acme-challenge/目录下'
|
||||||
|
}else if (form.challengeType === 'http'){
|
||||||
|
return '给每个域名单独配置dns提供商'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
domainsVerifyPlan!: DomainsVerifyPlanInput;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "Google公共EAB授权",
|
||||||
|
isSys: true,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
googleCommonEabAccessId!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "ZeroSSL公共EAB授权",
|
||||||
|
isSys: true,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
zerosslCommonEabAccessId!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "SSL.com公共EAB授权",
|
||||||
|
isSys: true,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
sslcomCommonEabAccessId!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "litessl公共EAB授权",
|
||||||
|
isSys: true,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
litesslCommonEabAccessId!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "EAB授权",
|
||||||
|
component: {
|
||||||
|
name: "access-selector",
|
||||||
|
type: "eab",
|
||||||
|
},
|
||||||
|
maybeNeed: true,
|
||||||
|
required: false,
|
||||||
|
helper:
|
||||||
|
"需要提供EAB授权" +
|
||||||
|
"\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'" +
|
||||||
|
"\nGoogle:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱" +
|
||||||
|
"\nSSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api credentials链接,然后点击编辑按钮,查看Secret key和HMAC key" +
|
||||||
|
"\nlitessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB",
|
||||||
|
mergeScript: `
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId)
|
||||||
|
|| (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|
||||||
|
|| (form.sslProvider === 'sslcom' && !form.sslcomCommonEabAccessId)
|
||||||
|
|| (form.sslProvider === 'litessl' && !form.litesslCommonEabAccessId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
eabAccessId!: number;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "服务账号授权",
|
||||||
|
component: {
|
||||||
|
name: "access-selector",
|
||||||
|
type: "google",
|
||||||
|
},
|
||||||
|
maybeNeed: true,
|
||||||
|
required: false,
|
||||||
|
helper: "google服务账号授权与EAB授权选填其中一个,[服务账号授权获取方法](https://certd.docmirror.cn/guide/use/google/)\n服务账号授权需要配置代理或者服务器本身在海外",
|
||||||
|
mergeScript: `
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.sslProvider === 'google' && !form.googleCommonEabAccessId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
googleAccessId!: 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: "如无特殊需求,默认即可\n选择RSA 2048 pkcs1可以获得旧版RSA证书",
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
privateKeyType!: PrivateKeyType;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "证书配置",
|
||||||
|
value: "classic",
|
||||||
|
component: {
|
||||||
|
name: "a-select",
|
||||||
|
vModel: "value",
|
||||||
|
options: [
|
||||||
|
{ value: "classic", label: "经典(classic)" },
|
||||||
|
{ value: "tlsserver", label: "TLS服务器(tlsserver)" },
|
||||||
|
{ value: "shortlived", label: "短暂的(shortlived)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
helper: "如无特殊需求,默认即可",
|
||||||
|
required: false,
|
||||||
|
mergeScript: `
|
||||||
|
return {
|
||||||
|
show: ctx.compute(({form})=>{
|
||||||
|
return form.sslProvider === 'letsencrypt'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
certProfile!: string;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "首选链",
|
||||||
|
component: {
|
||||||
|
name: "a-select",
|
||||||
|
vModel: "value",
|
||||||
|
options: preferredChainConfigs.letsencrypt.options,
|
||||||
|
},
|
||||||
|
helper: preferredChainConfigs.letsencrypt.helper,
|
||||||
|
required: false,
|
||||||
|
mergeScript: preferredChainMergeScript,
|
||||||
|
})
|
||||||
|
preferredChain!: string;
|
||||||
|
|
||||||
|
@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: "自定义反代地址",
|
||||||
|
component: {
|
||||||
|
placeholder: "google.yourproxy.com",
|
||||||
|
},
|
||||||
|
helper: "填写你的自定义反代地址,不要带http://\nletsencrypt反代目标:acme-v02.api.letsencrypt.org\ngoogle反代目标:dv.acme-v02.api.pki.goog",
|
||||||
|
})
|
||||||
|
reverseProxy = "";
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "跳过本地校验DNS",
|
||||||
|
value: false,
|
||||||
|
component: {
|
||||||
|
name: "a-switch",
|
||||||
|
vModel: "checked",
|
||||||
|
},
|
||||||
|
helper: "跳过本地校验可以加快申请速度,同时也会增加失败概率。",
|
||||||
|
})
|
||||||
|
skipLocalVerify = false;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "检查解析重试次数",
|
||||||
|
value: 20,
|
||||||
|
component: {
|
||||||
|
name: "a-input-number",
|
||||||
|
vModel: "value",
|
||||||
|
},
|
||||||
|
helper: "检查域名验证解析记录重试次数,如果你的域名服务商解析生效速度慢,可以适当增加此值",
|
||||||
|
})
|
||||||
|
maxCheckRetryCount = 20;
|
||||||
|
|
||||||
|
@TaskInput({
|
||||||
|
title: "等待解析生效时长",
|
||||||
|
value: 30,
|
||||||
|
component: {
|
||||||
|
name: "a-input-number",
|
||||||
|
vModel: "value",
|
||||||
|
},
|
||||||
|
helper: "等待解析生效时长(秒),如果使用CNAME方式校验,本地验证失败,可以尝试延长此时间(比如5-10分钟)",
|
||||||
|
})
|
||||||
|
waitDnsDiffuseTime = 30;
|
||||||
|
|
||||||
|
acme!: AcmeService;
|
||||||
|
|
||||||
|
eab!: EabAccess;
|
||||||
|
|
||||||
|
async onInit() {
|
||||||
|
let eab: EabAccess = null;
|
||||||
|
|
||||||
|
if (this.sslProvider && !this.sslProvider.startsWith("letsencrypt")) {
|
||||||
|
if (this.sslProvider === "google" && this.googleAccessId) {
|
||||||
|
this.logger.info("当前正在使用 google服务账号授权获取EAB");
|
||||||
|
const googleAccess = await this.getAccess(this.googleAccessId);
|
||||||
|
const googleClient = new GoogleClient({
|
||||||
|
access: googleAccess,
|
||||||
|
logger: this.logger,
|
||||||
|
});
|
||||||
|
eab = await googleClient.getEab();
|
||||||
|
} else {
|
||||||
|
const getEab = async (type: string) => {
|
||||||
|
if (this.eabAccessId) {
|
||||||
|
this.logger.info(`当前正在使用 ${type} EAB授权`);
|
||||||
|
eab = await this.getAccess(this.eabAccessId);
|
||||||
|
} else if (this[`${type}CommonEabAccessId`]) {
|
||||||
|
this.logger.info(`当前正在使用 ${type} 公共EAB授权`);
|
||||||
|
eab = await this.getAccess(this[`${type}CommonEabAccessId`], true);
|
||||||
|
} else {
|
||||||
|
throw new Error(`${type}需要配置EAB授权`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await getEab(this.sslProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.eab = eab;
|
||||||
|
const subDomainsGetter = await this.ctx.serviceGetter.get<ISubDomainsGetter>("subDomainsGetter");
|
||||||
|
const domainParser = new DomainParser(subDomainsGetter, this.logger);
|
||||||
|
this.acme = new AcmeService({
|
||||||
|
userId: this.ctx.user.id,
|
||||||
|
userContext: this.userContext,
|
||||||
|
logger: this.logger,
|
||||||
|
sslProvider: this.sslProvider,
|
||||||
|
eab,
|
||||||
|
skipLocalVerify: this.skipLocalVerify,
|
||||||
|
useMappingProxy: this.useProxy,
|
||||||
|
reverseProxy: this.reverseProxy,
|
||||||
|
privateKeyType: this.privateKeyType,
|
||||||
|
signal: this.ctx.signal,
|
||||||
|
maxCheckRetryCount: this.maxCheckRetryCount,
|
||||||
|
domainParser,
|
||||||
|
waitDnsDiffuseTime: this.waitDnsDiffuseTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCertApply() {
|
||||||
|
let email = this.email;
|
||||||
|
if (this.eab && this.eab.email) {
|
||||||
|
email = this.eab.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: IDnsProvider = null;
|
||||||
|
let domainsVerifyPlan: DomainsVerifyPlan = null;
|
||||||
|
if (this.challengeType === "cname" || this.challengeType === "http" || this.challengeType === "dnses") {
|
||||||
|
domainsVerifyPlan = await this.createDomainsVerifyPlan(domains, this.domainsVerifyPlan);
|
||||||
|
} else if (this.challengeType === "auto") {
|
||||||
|
domainsVerifyPlan = await this.createDomainsVerifyPlanByAuto(domains);
|
||||||
|
} else {
|
||||||
|
const dnsProviderType = this.dnsProviderType;
|
||||||
|
const access = await this.getAccess(this.dnsProviderAccess);
|
||||||
|
dnsProvider = await this.createDnsProvider(dnsProviderType, access);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cert = await this.acme.order({
|
||||||
|
email,
|
||||||
|
domains,
|
||||||
|
dnsProvider,
|
||||||
|
domainsVerifyPlan,
|
||||||
|
csrInfo,
|
||||||
|
privateKeyType: this.privateKeyType,
|
||||||
|
profile: this.certProfile,
|
||||||
|
preferredChain: this.preferredChain,
|
||||||
|
});
|
||||||
|
|
||||||
|
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})`);
|
||||||
|
}
|
||||||
|
if (e.name === "CancelError") {
|
||||||
|
throw new CancelError(e.message);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
|
||||||
|
const domainParser = this.acme.options.domainParser;
|
||||||
|
const context: DnsProviderContext = {
|
||||||
|
access: dnsProviderAccess,
|
||||||
|
logger: this.logger,
|
||||||
|
http: this.ctx.http,
|
||||||
|
utils,
|
||||||
|
domainParser,
|
||||||
|
serviceGetter: this.ctx.serviceGetter,
|
||||||
|
};
|
||||||
|
return await createDnsProvider({
|
||||||
|
dnsProviderType,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDomainsVerifyPlan(domains: string[], verifyPlanSetting: DomainsVerifyPlanInput): Promise<DomainsVerifyPlan> {
|
||||||
|
const plan: DomainsVerifyPlan = {};
|
||||||
|
|
||||||
|
const domainParser = this.acme.options.domainParser;
|
||||||
|
for (const fullDomain of domains) {
|
||||||
|
const domain = fullDomain.replaceAll("*.", "");
|
||||||
|
const mainDomain = await domainParser.parse(domain);
|
||||||
|
const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain];
|
||||||
|
if (planSetting == null) {
|
||||||
|
throw new Error(`没有找到域名(${domain})的校验计划(如果您在流水线创建之后设置了子域名托管,需要重新编辑证书申请任务和重新校验cname记录的校验状态)`);
|
||||||
|
}
|
||||||
|
if (planSetting.type === "dns") {
|
||||||
|
plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain);
|
||||||
|
} else if (planSetting.type === "cname") {
|
||||||
|
plan[domain] = await this.createCnameDomainVerifyPlan(domain, mainDomain);
|
||||||
|
} else if (planSetting.type === "http") {
|
||||||
|
plan[domain] = await this.createHttpDomainVerifyPlan(planSetting.httpVerifyPlan[domain], domain, mainDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDomainsVerifyPlanByAuto(domains: string[]) {
|
||||||
|
//从数据库里面自动选择校验方式
|
||||||
|
// domain list
|
||||||
|
const domainList = new Set<string>();
|
||||||
|
//整理域名
|
||||||
|
for (let domain of domains) {
|
||||||
|
domain = domain.replaceAll("*.", "");
|
||||||
|
domainList.add(domain);
|
||||||
|
}
|
||||||
|
const domainVerifierGetter: IDomainVerifierGetter = await this.ctx.serviceGetter.get("domainVerifierGetter");
|
||||||
|
|
||||||
|
const verifiers: DomainVerifiers = await domainVerifierGetter.getVerifiers([...domainList]);
|
||||||
|
|
||||||
|
const plan: DomainsVerifyPlan = {};
|
||||||
|
|
||||||
|
for (const domain in verifiers) {
|
||||||
|
const verifier = verifiers[domain];
|
||||||
|
if (verifier == null) {
|
||||||
|
throw new Error(`没有找到与该域名(${domain})匹配的校验方式,请先到‘域名管理’页面添加校验方式`);
|
||||||
|
}
|
||||||
|
if (verifier.type === "dns") {
|
||||||
|
plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain);
|
||||||
|
} else if (verifier.type === "cname") {
|
||||||
|
plan[domain] = await this.createCnameDomainVerifyPlan(domain, verifier.mainDomain);
|
||||||
|
} else if (verifier.type === "http") {
|
||||||
|
plan[domain] = await this.createHttpDomainVerifyPlan(verifier.http, domain, verifier.mainDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDnsDomainVerifyPlan(planSetting: DnsVerifier, domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
|
||||||
|
const access = await this.getAccess(planSetting.dnsProviderAccessId);
|
||||||
|
return {
|
||||||
|
type: "dns",
|
||||||
|
mainDomain,
|
||||||
|
domain,
|
||||||
|
dnsProvider: await this.createDnsProvider(planSetting.dnsProviderType, access),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createHttpDomainVerifyPlan(httpSetting: HttpVerifier, domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
|
||||||
|
const httpUploaderContext = {
|
||||||
|
accessService: this.ctx.accessService,
|
||||||
|
logger: this.logger,
|
||||||
|
utils,
|
||||||
|
};
|
||||||
|
|
||||||
|
const access = await this.getAccess(httpSetting.httpUploaderAccess);
|
||||||
|
let rootDir = httpSetting.httpUploadRootDir;
|
||||||
|
if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) {
|
||||||
|
rootDir = rootDir + "/";
|
||||||
|
}
|
||||||
|
this.logger.info("上传方式", httpSetting.httpUploaderType);
|
||||||
|
const httpUploader = await ossClientFactory.createOssClientByType(httpSetting.httpUploaderType, {
|
||||||
|
access,
|
||||||
|
rootDir: rootDir,
|
||||||
|
ctx: httpUploaderContext,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: "http",
|
||||||
|
domain,
|
||||||
|
mainDomain,
|
||||||
|
httpVerifyPlan: {
|
||||||
|
type: "http",
|
||||||
|
domain,
|
||||||
|
httpUploader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createCnameDomainVerifyPlan(domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
|
||||||
|
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(domain);
|
||||||
|
if (cnameRecord == null) {
|
||||||
|
throw new Error(`请先配置${domain}的CNAME记录,并通过校验`);
|
||||||
|
}
|
||||||
|
if (cnameRecord.status !== "valid") {
|
||||||
|
throw new Error(`CNAME记录${domain}的校验状态为${cnameRecord.status},请等待校验通过`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主域名异常
|
||||||
|
if (cnameRecord.mainDomain && mainDomain && cnameRecord.mainDomain !== mainDomain) {
|
||||||
|
throw new Error(`CNAME记录${domain}的域名与配置的主域名不一致(${cnameRecord.mainDomain}≠${mainDomain}),请确认是否在流水线创建之后修改了子域名托管,您需要重新校验CNAME记录的校验状态`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dnsProvider = cnameRecord.commonDnsProvider;
|
||||||
|
if (cnameRecord.cnameProvider.id > 0) {
|
||||||
|
dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "cname",
|
||||||
|
domain,
|
||||||
|
mainDomain,
|
||||||
|
cnameVerifyPlan: {
|
||||||
|
domain: cnameRecord.cnameProvider.domain,
|
||||||
|
fullRecord: cnameRecord.recordValue,
|
||||||
|
dnsProvider,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new CertApplyPlugin();
|
||||||
@@ -3,10 +3,6 @@ import type { CertInfo } from "../acme.js";
|
|||||||
import { CertReader } from "@certd/plugin-lib";
|
import { CertReader } from "@certd/plugin-lib";
|
||||||
import { CertApplyBaseConvertPlugin } from "../base-convert.js";
|
import { CertApplyBaseConvertPlugin } from "../base-convert.js";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export { CertReader };
|
|
||||||
export type { CertInfo };
|
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: "CertApplyUpload",
|
name: "CertApplyUpload",
|
||||||
icon: "ph:certificate",
|
icon: "ph:certificate",
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
import { IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import { AliyunAccess } from "../../../../plugin-lib/aliyun/access/index.js";
|
import { AliyunAccess } from "../../../../plugin-lib/aliyun/access/index.js";
|
||||||
import type { CertInfo } from "../acme.js";
|
|
||||||
import { CertApplyBasePlugin } from "../base.js";
|
import { CertApplyBasePlugin } from "../base.js";
|
||||||
import { CertReader, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
import { CertReader, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export { CertReader };
|
|
||||||
export type { CertInfo };
|
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: "CertApplyGetFormAliyun",
|
name: "CertApplyGetFormAliyun",
|
||||||
icon: "ph:certificate",
|
icon: "ph:certificate",
|
||||||
|
|||||||
@@ -1,668 +1,3 @@
|
|||||||
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
|
||||||
import { utils } from "@certd/basic";
|
|
||||||
|
|
||||||
import { AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
|
|
||||||
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib";
|
|
||||||
import { CertReader } from "@certd/plugin-lib";
|
|
||||||
import { CertApplyBasePlugin } from "./base.js";
|
|
||||||
import { GoogleClient } from "../../libs/google.js";
|
|
||||||
import { EabAccess } from "../../access/index.js";
|
|
||||||
import { DomainParser } from "@certd/plugin-lib";
|
|
||||||
import { ossClientFactory } from "../../../plugin-lib/oss/factory.js";
|
|
||||||
import { merge } from "lodash-es";
|
|
||||||
|
|
||||||
export * from "./base.js";
|
export * from "./base.js";
|
||||||
export type CnameRecordInput = {
|
export * from "./apply.js";
|
||||||
id: number;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type HttpRecordInput = {
|
|
||||||
domain: string;
|
|
||||||
httpUploaderType: string;
|
|
||||||
httpUploaderAccess: number;
|
|
||||||
httpUploadRootDir: string;
|
|
||||||
};
|
|
||||||
export type DomainVerifyPlanInput = {
|
|
||||||
domain: string;
|
|
||||||
type: "cname" | "dns" | "http";
|
|
||||||
dnsProviderType?: string;
|
|
||||||
dnsProviderAccessType?: string;
|
|
||||||
dnsProviderAccessId?: number;
|
|
||||||
cnameVerifyPlan?: Record<string, CnameRecordInput>;
|
|
||||||
httpVerifyPlan?: Record<string, HttpRecordInput>;
|
|
||||||
};
|
|
||||||
export type DomainsVerifyPlanInput = {
|
|
||||||
[key: string]: DomainVerifyPlanInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
const preferredChainConfigs = {
|
|
||||||
letsencrypt: {
|
|
||||||
helper: "如无特殊需求保持默认即可",
|
|
||||||
options: [
|
|
||||||
{ value: "ISRG Root X1", label: "ISRG Root X1" },
|
|
||||||
{ value: "ISRG Root X2", label: "ISRG Root X2" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
google: {
|
|
||||||
helper: "GlobalSign 提供对老旧设备更好的兼容性,但证书链会变长",
|
|
||||||
options: [
|
|
||||||
{ value: "GTS Root R1", label: "GTS Root R1" },
|
|
||||||
{ value: "GlobalSign", label: "GlobalSign" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
|
|
||||||
|
|
||||||
const preferredChainMergeScript = (() => {
|
|
||||||
const configs = JSON.stringify(preferredChainConfigs);
|
|
||||||
const supportedProviders = JSON.stringify(preferredChainSupportedProviders);
|
|
||||||
const defaultProvider = JSON.stringify(preferredChainSupportedProviders[0]);
|
|
||||||
return `
|
|
||||||
const chainConfigs = ${configs};
|
|
||||||
const supportedProviders = ${supportedProviders};
|
|
||||||
const defaultProvider = ${defaultProvider};
|
|
||||||
const getConfig = (provider)=> chainConfigs[provider] || chainConfigs[defaultProvider];
|
|
||||||
return {
|
|
||||||
show: ctx.compute(({form})=> supportedProviders.includes(form.sslProvider)),
|
|
||||||
component: {
|
|
||||||
options: ctx.compute(({form})=> getConfig(form.sslProvider).options)
|
|
||||||
},
|
|
||||||
helper: ctx.compute(({form})=> getConfig(form.sslProvider).helper),
|
|
||||||
value: ctx.compute(({form})=>{
|
|
||||||
const { options } = getConfig(form.sslProvider);
|
|
||||||
const allowed = options.map(item=>item.value);
|
|
||||||
const current = form.preferredChain;
|
|
||||||
if(allowed.includes(current)){
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
return allowed[0];
|
|
||||||
})
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
})();
|
|
||||||
|
|
||||||
@IsTaskPlugin({
|
|
||||||
name: "CertApply",
|
|
||||||
title: "证书申请(JS版)",
|
|
||||||
icon: "ph:certificate",
|
|
||||||
group: pluginGroups.cert.key,
|
|
||||||
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
|
|
||||||
default: {
|
|
||||||
input: {
|
|
||||||
renewDays: 18,
|
|
||||||
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代理验证" },
|
|
||||||
{ value: "http", label: "HTTP文件验证(IP证书只能选它)" },
|
|
||||||
{ value: "dnses", label: "多DNS提供商" },
|
|
||||||
{ value: "auto", label: "自动匹配" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
helper: `1. <b>DNS直接验证</b>:当域名dns解析已被本系统支持时(即下方DNS解析服务商选项中可选),推荐选择此方式
|
|
||||||
2. <b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(如果经常申请失败,建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证)
|
|
||||||
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
|
|
||||||
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
|
|
||||||
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
challengeType!: string;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "证书颁发机构",
|
|
||||||
value: "letsencrypt",
|
|
||||||
component: {
|
|
||||||
name: "icon-select",
|
|
||||||
vModel: "value",
|
|
||||||
options: [
|
|
||||||
{ value: "letsencrypt", label: "Let's Encrypt(免费,新手推荐,支持IP证书)", icon: "simple-icons:letsencrypt" },
|
|
||||||
{ value: "google", label: "Google(免费)", icon: "flat-color-icons:google" },
|
|
||||||
{ value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" },
|
|
||||||
{ value: "litessl", label: "litessl(免费)", icon: "roentgen:free" },
|
|
||||||
{ value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" },
|
|
||||||
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境(仅供测试)", icon: "simple-icons:letsencrypt" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,仅首次需要翻墙获取EAB授权\nZeroSSL:需要EAB授权,无需翻墙\nSSL.com:仅主域名和www免费,必须设置CAA记录",
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
sslProvider!: SSLProvider;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "DNS解析服务商",
|
|
||||||
component: {
|
|
||||||
name: "dns-provider-selector",
|
|
||||||
},
|
|
||||||
mergeScript: `
|
|
||||||
return {
|
|
||||||
show: ctx.compute(({form})=>{
|
|
||||||
return form.challengeType === 'dns'
|
|
||||||
}),
|
|
||||||
component:{
|
|
||||||
onSelectedChange: ctx.compute(({form})=>{
|
|
||||||
return ($event)=>{
|
|
||||||
form.dnsProviderAccessType = $event.accessType
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
required: true,
|
|
||||||
helper: "您的域名注册商,或者域名的dns服务器属于哪个平台\n如果这里没有,请选择CNAME代理验证校验方式",
|
|
||||||
})
|
|
||||||
dnsProviderType!: string;
|
|
||||||
|
|
||||||
// dns解析授权类型,勿删
|
|
||||||
dnsProviderAccessType!: string;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "DNS解析授权",
|
|
||||||
component: {
|
|
||||||
name: "access-selector",
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
helper: "请选择dns解析服务商授权",
|
|
||||||
mergeScript: `return {
|
|
||||||
component:{
|
|
||||||
type: ctx.compute(({form})=>{
|
|
||||||
return form.dnsProviderAccessType || form.dnsProviderType
|
|
||||||
})
|
|
||||||
},
|
|
||||||
show: ctx.compute(({form})=>{
|
|
||||||
return form.challengeType === 'dns'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
dnsProviderAccess!: number;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "域名验证配置",
|
|
||||||
component: {
|
|
||||||
name: "domains-verify-plan-editor",
|
|
||||||
},
|
|
||||||
rules: [{ type: "checkDomainVerifyPlan" }],
|
|
||||||
required: true,
|
|
||||||
col: {
|
|
||||||
span: 24,
|
|
||||||
},
|
|
||||||
mergeScript: `return {
|
|
||||||
component:{
|
|
||||||
domains: ctx.compute(({form})=>{
|
|
||||||
return form.domains
|
|
||||||
}),
|
|
||||||
defaultType: ctx.compute(({form})=>{
|
|
||||||
return form.challengeType || 'cname'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
show: ctx.compute(({form})=>{
|
|
||||||
return form.challengeType === 'cname' || form.challengeType === 'http' || form.challengeType === 'dnses'
|
|
||||||
}),
|
|
||||||
helper: ctx.compute(({form})=>{
|
|
||||||
if(form.challengeType === 'cname' ){
|
|
||||||
return '请按照上面的提示,给要申请证书的域名添加CNAME记录,添加后,点击验证,验证成功后不要删除记录,申请和续期证书会一直用它'
|
|
||||||
}else if (form.challengeType === 'http'){
|
|
||||||
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录的.well-known/acme-challenge/目录下'
|
|
||||||
}else if (form.challengeType === 'http'){
|
|
||||||
return '给每个域名单独配置dns提供商'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
domainsVerifyPlan!: DomainsVerifyPlanInput;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "Google公共EAB授权",
|
|
||||||
isSys: true,
|
|
||||||
show: false,
|
|
||||||
})
|
|
||||||
googleCommonEabAccessId!: number;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "ZeroSSL公共EAB授权",
|
|
||||||
isSys: true,
|
|
||||||
show: false,
|
|
||||||
})
|
|
||||||
zerosslCommonEabAccessId!: number;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "SSL.com公共EAB授权",
|
|
||||||
isSys: true,
|
|
||||||
show: false,
|
|
||||||
})
|
|
||||||
sslcomCommonEabAccessId!: number;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "litessl公共EAB授权",
|
|
||||||
isSys: true,
|
|
||||||
show: false,
|
|
||||||
})
|
|
||||||
litesslCommonEabAccessId!: number;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "EAB授权",
|
|
||||||
component: {
|
|
||||||
name: "access-selector",
|
|
||||||
type: "eab",
|
|
||||||
},
|
|
||||||
maybeNeed: true,
|
|
||||||
required: false,
|
|
||||||
helper:
|
|
||||||
"需要提供EAB授权" +
|
|
||||||
"\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'" +
|
|
||||||
"\nGoogle:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱" +
|
|
||||||
"\nSSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api credentials链接,然后点击编辑按钮,查看Secret key和HMAC key" +
|
|
||||||
"\nlitessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB",
|
|
||||||
mergeScript: `
|
|
||||||
return {
|
|
||||||
show: ctx.compute(({form})=>{
|
|
||||||
return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId)
|
|
||||||
|| (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|
|
||||||
|| (form.sslProvider === 'sslcom' && !form.sslcomCommonEabAccessId)
|
|
||||||
|| (form.sslProvider === 'litessl' && !form.litesslCommonEabAccessId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
eabAccessId!: number;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "服务账号授权",
|
|
||||||
component: {
|
|
||||||
name: "access-selector",
|
|
||||||
type: "google",
|
|
||||||
},
|
|
||||||
maybeNeed: true,
|
|
||||||
required: false,
|
|
||||||
helper: "google服务账号授权与EAB授权选填其中一个,[服务账号授权获取方法](https://certd.docmirror.cn/guide/use/google/)\n服务账号授权需要配置代理或者服务器本身在海外",
|
|
||||||
mergeScript: `
|
|
||||||
return {
|
|
||||||
show: ctx.compute(({form})=>{
|
|
||||||
return form.sslProvider === 'google' && !form.googleCommonEabAccessId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
googleAccessId!: 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: "如无特殊需求,默认即可\n选择RSA 2048 pkcs1可以获得旧版RSA证书",
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
privateKeyType!: PrivateKeyType;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "证书配置",
|
|
||||||
value: "classic",
|
|
||||||
component: {
|
|
||||||
name: "a-select",
|
|
||||||
vModel: "value",
|
|
||||||
options: [
|
|
||||||
{ value: "classic", label: "经典(classic)" },
|
|
||||||
{ value: "tlsserver", label: "TLS服务器(tlsserver)" },
|
|
||||||
{ value: "shortlived", label: "短暂的(shortlived)" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
helper: "如无特殊需求,默认即可",
|
|
||||||
required: false,
|
|
||||||
mergeScript: `
|
|
||||||
return {
|
|
||||||
show: ctx.compute(({form})=>{
|
|
||||||
return form.sslProvider === 'letsencrypt'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
certProfile!: string;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "首选链",
|
|
||||||
component: {
|
|
||||||
name: "a-select",
|
|
||||||
vModel: "value",
|
|
||||||
options: preferredChainConfigs.letsencrypt.options,
|
|
||||||
},
|
|
||||||
helper: preferredChainConfigs.letsencrypt.helper,
|
|
||||||
required: false,
|
|
||||||
mergeScript: preferredChainMergeScript,
|
|
||||||
})
|
|
||||||
preferredChain!: string;
|
|
||||||
|
|
||||||
@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: "自定义反代地址",
|
|
||||||
component: {
|
|
||||||
placeholder: "google.yourproxy.com",
|
|
||||||
},
|
|
||||||
helper: "填写你的自定义反代地址,不要带http://\nletsencrypt反代目标:acme-v02.api.letsencrypt.org\ngoogle反代目标:dv.acme-v02.api.pki.goog",
|
|
||||||
})
|
|
||||||
reverseProxy = "";
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "跳过本地校验DNS",
|
|
||||||
value: false,
|
|
||||||
component: {
|
|
||||||
name: "a-switch",
|
|
||||||
vModel: "checked",
|
|
||||||
},
|
|
||||||
helper: "跳过本地校验可以加快申请速度,同时也会增加失败概率。",
|
|
||||||
})
|
|
||||||
skipLocalVerify = false;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "检查解析重试次数",
|
|
||||||
value: 20,
|
|
||||||
component: {
|
|
||||||
name: "a-input-number",
|
|
||||||
vModel: "value",
|
|
||||||
},
|
|
||||||
helper: "检查域名验证解析记录重试次数,如果你的域名服务商解析生效速度慢,可以适当增加此值",
|
|
||||||
})
|
|
||||||
maxCheckRetryCount = 20;
|
|
||||||
|
|
||||||
@TaskInput({
|
|
||||||
title: "等待解析生效时长",
|
|
||||||
value: 30,
|
|
||||||
component: {
|
|
||||||
name: "a-input-number",
|
|
||||||
vModel: "value",
|
|
||||||
},
|
|
||||||
helper: "等待解析生效时长(秒),如果使用CNAME方式校验,本地验证失败,可以尝试延长此时间(比如5-10分钟)",
|
|
||||||
})
|
|
||||||
waitDnsDiffuseTime = 30;
|
|
||||||
|
|
||||||
acme!: AcmeService;
|
|
||||||
|
|
||||||
eab!: EabAccess;
|
|
||||||
|
|
||||||
async onInit() {
|
|
||||||
let eab: EabAccess = null;
|
|
||||||
|
|
||||||
if (this.sslProvider && !this.sslProvider.startsWith("letsencrypt")) {
|
|
||||||
if (this.sslProvider === "google" && this.googleAccessId) {
|
|
||||||
this.logger.info("当前正在使用 google服务账号授权获取EAB");
|
|
||||||
const googleAccess = await this.getAccess(this.googleAccessId);
|
|
||||||
const googleClient = new GoogleClient({
|
|
||||||
access: googleAccess,
|
|
||||||
logger: this.logger,
|
|
||||||
});
|
|
||||||
eab = await googleClient.getEab();
|
|
||||||
} else {
|
|
||||||
const getEab = async (type: string) => {
|
|
||||||
if (this.eabAccessId) {
|
|
||||||
this.logger.info(`当前正在使用 ${type} EAB授权`);
|
|
||||||
eab = await this.getAccess(this.eabAccessId);
|
|
||||||
} else if (this[`${type}CommonEabAccessId`]) {
|
|
||||||
this.logger.info(`当前正在使用 ${type} 公共EAB授权`);
|
|
||||||
eab = await this.getAccess(this[`${type}CommonEabAccessId`], true);
|
|
||||||
} else {
|
|
||||||
throw new Error(`${type}需要配置EAB授权`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
await getEab(this.sslProvider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.eab = eab;
|
|
||||||
const subDomainsGetter = await this.ctx.serviceGetter.get<ISubDomainsGetter>("subDomainsGetter");
|
|
||||||
const domainParser = new DomainParser(subDomainsGetter, this.logger);
|
|
||||||
this.acme = new AcmeService({
|
|
||||||
userId: this.ctx.user.id,
|
|
||||||
userContext: this.userContext,
|
|
||||||
logger: this.logger,
|
|
||||||
sslProvider: this.sslProvider,
|
|
||||||
eab,
|
|
||||||
skipLocalVerify: this.skipLocalVerify,
|
|
||||||
useMappingProxy: this.useProxy,
|
|
||||||
reverseProxy: this.reverseProxy,
|
|
||||||
privateKeyType: this.privateKeyType,
|
|
||||||
signal: this.ctx.signal,
|
|
||||||
maxCheckRetryCount: this.maxCheckRetryCount,
|
|
||||||
domainParser,
|
|
||||||
waitDnsDiffuseTime: this.waitDnsDiffuseTime,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async doCertApply() {
|
|
||||||
let email = this.email;
|
|
||||||
if (this.eab && this.eab.email) {
|
|
||||||
email = this.eab.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: IDnsProvider = null;
|
|
||||||
let domainsVerifyPlan: DomainsVerifyPlan = null;
|
|
||||||
if (this.challengeType === "cname" || this.challengeType === "http" || this.challengeType === "dnses") {
|
|
||||||
domainsVerifyPlan = await this.createDomainsVerifyPlan(domains, this.domainsVerifyPlan);
|
|
||||||
} else if (this.challengeType === "auto") {
|
|
||||||
domainsVerifyPlan = await this.createDomainsVerifyPlanByAuto(domains);
|
|
||||||
} else {
|
|
||||||
const dnsProviderType = this.dnsProviderType;
|
|
||||||
const access = await this.getAccess(this.dnsProviderAccess);
|
|
||||||
dnsProvider = await this.createDnsProvider(dnsProviderType, access);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cert = await this.acme.order({
|
|
||||||
email,
|
|
||||||
domains,
|
|
||||||
dnsProvider,
|
|
||||||
domainsVerifyPlan,
|
|
||||||
csrInfo,
|
|
||||||
privateKeyType: this.privateKeyType,
|
|
||||||
profile: this.certProfile,
|
|
||||||
preferredChain: this.preferredChain,
|
|
||||||
});
|
|
||||||
|
|
||||||
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})`);
|
|
||||||
}
|
|
||||||
if (e.name === "CancelError") {
|
|
||||||
throw new CancelError(e.message);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
|
|
||||||
const domainParser = this.acme.options.domainParser;
|
|
||||||
const context: DnsProviderContext = {
|
|
||||||
access: dnsProviderAccess,
|
|
||||||
logger: this.logger,
|
|
||||||
http: this.ctx.http,
|
|
||||||
utils,
|
|
||||||
domainParser,
|
|
||||||
serviceGetter: this.ctx.serviceGetter,
|
|
||||||
};
|
|
||||||
return await createDnsProvider({
|
|
||||||
dnsProviderType,
|
|
||||||
context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createDomainsVerifyPlan(domains: string[], verifyPlanSetting: DomainsVerifyPlanInput): Promise<DomainsVerifyPlan> {
|
|
||||||
const plan: DomainsVerifyPlan = {};
|
|
||||||
|
|
||||||
const domainParser = this.acme.options.domainParser;
|
|
||||||
for (const fullDomain of domains) {
|
|
||||||
const domain = fullDomain.replaceAll("*.", "");
|
|
||||||
const mainDomain = await domainParser.parse(domain);
|
|
||||||
const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain];
|
|
||||||
if (planSetting == null) {
|
|
||||||
throw new Error(`没有找到域名(${domain})的校验计划(如果您在流水线创建之后设置了子域名托管,需要重新编辑证书申请任务和重新校验cname记录的校验状态)`);
|
|
||||||
}
|
|
||||||
if (planSetting.type === "dns") {
|
|
||||||
plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain);
|
|
||||||
} else if (planSetting.type === "cname") {
|
|
||||||
plan[domain] = await this.createCnameDomainVerifyPlan(domain, mainDomain);
|
|
||||||
} else if (planSetting.type === "http") {
|
|
||||||
plan[domain] = await this.createHttpDomainVerifyPlan(planSetting.httpVerifyPlan[domain], domain, mainDomain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createDomainsVerifyPlanByAuto(domains: string[]) {
|
|
||||||
//从数据库里面自动选择校验方式
|
|
||||||
// domain list
|
|
||||||
const domainList = new Set<string>();
|
|
||||||
//整理域名
|
|
||||||
for (let domain of domains) {
|
|
||||||
domain = domain.replaceAll("*.", "");
|
|
||||||
domainList.add(domain);
|
|
||||||
}
|
|
||||||
const domainVerifierGetter: IDomainVerifierGetter = await this.ctx.serviceGetter.get("domainVerifierGetter");
|
|
||||||
|
|
||||||
const verifiers: DomainVerifiers = await domainVerifierGetter.getVerifiers([...domainList]);
|
|
||||||
|
|
||||||
const plan: DomainsVerifyPlan = {};
|
|
||||||
|
|
||||||
for (const domain in verifiers) {
|
|
||||||
const verifier = verifiers[domain];
|
|
||||||
if (verifier == null) {
|
|
||||||
throw new Error(`没有找到与该域名(${domain})匹配的校验方式,请先到‘域名管理’页面添加校验方式`);
|
|
||||||
}
|
|
||||||
if (verifier.type === "dns") {
|
|
||||||
plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain);
|
|
||||||
} else if (verifier.type === "cname") {
|
|
||||||
plan[domain] = await this.createCnameDomainVerifyPlan(domain, verifier.mainDomain);
|
|
||||||
} else if (verifier.type === "http") {
|
|
||||||
plan[domain] = await this.createHttpDomainVerifyPlan(verifier.http, domain, verifier.mainDomain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createDnsDomainVerifyPlan(planSetting: DnsVerifier, domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
|
|
||||||
const access = await this.getAccess(planSetting.dnsProviderAccessId);
|
|
||||||
return {
|
|
||||||
type: "dns",
|
|
||||||
mainDomain,
|
|
||||||
domain,
|
|
||||||
dnsProvider: await this.createDnsProvider(planSetting.dnsProviderType, access),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createHttpDomainVerifyPlan(httpSetting: HttpVerifier, domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
|
|
||||||
const httpUploaderContext = {
|
|
||||||
accessService: this.ctx.accessService,
|
|
||||||
logger: this.logger,
|
|
||||||
utils,
|
|
||||||
};
|
|
||||||
|
|
||||||
const access = await this.getAccess(httpSetting.httpUploaderAccess);
|
|
||||||
let rootDir = httpSetting.httpUploadRootDir;
|
|
||||||
if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) {
|
|
||||||
rootDir = rootDir + "/";
|
|
||||||
}
|
|
||||||
this.logger.info("上传方式", httpSetting.httpUploaderType);
|
|
||||||
const httpUploader = await ossClientFactory.createOssClientByType(httpSetting.httpUploaderType, {
|
|
||||||
access,
|
|
||||||
rootDir: rootDir,
|
|
||||||
ctx: httpUploaderContext,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
type: "http",
|
|
||||||
domain,
|
|
||||||
mainDomain,
|
|
||||||
httpVerifyPlan: {
|
|
||||||
type: "http",
|
|
||||||
domain,
|
|
||||||
httpUploader,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createCnameDomainVerifyPlan(domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
|
|
||||||
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(domain);
|
|
||||||
if (cnameRecord == null) {
|
|
||||||
throw new Error(`请先配置${domain}的CNAME记录,并通过校验`);
|
|
||||||
}
|
|
||||||
if (cnameRecord.status !== "valid") {
|
|
||||||
throw new Error(`CNAME记录${domain}的校验状态为${cnameRecord.status},请等待校验通过`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主域名异常
|
|
||||||
if (cnameRecord.mainDomain && mainDomain && cnameRecord.mainDomain !== mainDomain) {
|
|
||||||
throw new Error(`CNAME记录${domain}的域名与配置的主域名不一致(${cnameRecord.mainDomain}≠${mainDomain}),请确认是否在流水线创建之后修改了子域名托管,您需要重新校验CNAME记录的校验状态`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dnsProvider = cnameRecord.commonDnsProvider;
|
|
||||||
if (cnameRecord.cnameProvider.id > 0) {
|
|
||||||
dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "cname",
|
|
||||||
domain,
|
|
||||||
mainDomain,
|
|
||||||
cnameVerifyPlan: {
|
|
||||||
domain: cnameRecord.cnameProvider.domain,
|
|
||||||
fullRecord: cnameRecord.recordValue,
|
|
||||||
dnsProvider,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new CertApplyPlugin();
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const dnsList = [];
|
export const dnsList = [];
|
||||||
|
export type PrivateKeyType = "rsa2048" | "rsa3072" | "rsa4096" | "rsa8192" | "ec256" | "ec384";
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput } from "@certd/pipeline";
|
import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput } from "@certd/pipeline";
|
||||||
import type { CertInfo } from "../acme.js";
|
|
||||||
import { CertReader } from "@certd/plugin-lib";
|
import { CertReader } from "@certd/plugin-lib";
|
||||||
import { CertApplyBasePlugin } from "../base.js";
|
import { CertApplyBasePlugin } from "../base.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { EabAccess } from "../../../access/index.js";
|
import { EabAccess } from "../../../access/index.js";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
|
import { PrivateKeyType } from "./dns.js";
|
||||||
|
|
||||||
export { CertReader };
|
|
||||||
export type { CertInfo };
|
|
||||||
export type PrivateKeyType = "rsa2048" | "rsa3072" | "rsa4096" | "rsa8192" | "ec256" | "ec384";
|
|
||||||
|
|
||||||
@IsTaskPlugin({
|
@IsTaskPlugin({
|
||||||
name: "CertApplyLego",
|
name: "CertApplyLego",
|
||||||
|
|||||||
Reference in New Issue
Block a user