v2
xiaojunnuo 2025-01-03 00:12:15 +08:00
parent ec342708b2
commit 03b751fa13
13 changed files with 275 additions and 128 deletions

View File

@ -9,16 +9,24 @@ import { IDnsProvider, parseDomain } from "../../dns-provider/index.js";
import { HttpChallengeUploader } from "./uploads/api.js";
export type CnameVerifyPlan = {
type?: string;
domain: string;
fullRecord: string;
dnsProvider: IDnsProvider;
};
export type HttpVerifyPlan = {
type: string;
domain: string;
httpUploader: HttpChallengeUploader;
};
export type DomainVerifyPlan = {
domain: string;
type: "cname" | "dns" | "http";
dnsProvider?: IDnsProvider;
cnameVerifyPlan?: Record<string, CnameVerifyPlan>;
httpVerifyPlan?: Record<string, HttpVerifyPlan>;
};
export type DomainsVerifyPlan = {
[key: string]: DomainVerifyPlan;
@ -171,7 +179,23 @@ export class AcmeService {
const filePath = `.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization;
this.logger.info(`校验 ${fullDomain} ,准备上传文件:${filePath}`);
await providers.httpUploader.upload(filePath, fileContents);
let httpUploaderPlan: HttpVerifyPlan = null;
if (providers.domainsVerifyPlan) {
//查找文件上传配置
for (const mainDomain in providers.domainsVerifyPlan) {
const domainVerifyPlan = providers.domainsVerifyPlan[mainDomain];
if (domainVerifyPlan && domainVerifyPlan.type === "http" && domainVerifyPlan.httpVerifyPlan[fullDomain]) {
httpUploaderPlan = domainVerifyPlan.httpVerifyPlan[fullDomain];
break;
}
}
}
if (httpUploaderPlan == null) {
throw new Error(`未找到域名【${fullDomain}】的http校验计划`);
}
await httpUploaderPlan.httpUploader.upload(filePath, fileContents);
this.logger.info(`上传文件【${filePath}】成功`);
} else if (challenge.type === "dns-01") {
/* dns-01 */
@ -204,8 +228,11 @@ export class AcmeService {
} else {
this.logger.error("未找到域名Cname校验计划使用默认的dnsProvider");
}
} else if (domainVerifyPlan.type === "http") {
throw new Error("切换为http校验");
} else {
this.logger.error("不支持的校验类型", domainVerifyPlan.type);
// this.logger.error("不支持的校验类型", domainVerifyPlan.type);
throw new Error("不支持的校验类型", domainVerifyPlan.type);
}
} else {
this.logger.info("未找到域名校验计划使用默认的dnsProvider");
@ -346,7 +373,7 @@ export class AcmeService {
email: email,
termsOfServiceAgreed: true,
skipChallengeVerification: this.skipLocalVerify,
challengePriority: ["dns-01"],
challengePriority: ["dns-01", "http-01"],
challengeCreateFn: async (
authz: acme.Authorization,
challenge: Challenge,

View File

@ -1,7 +1,7 @@
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { utils } from "@certd/basic";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, HttpVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import * as _ from "lodash-es";
import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js";
@ -9,7 +9,6 @@ import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
import { GoogleClient } from "../../libs/google.js";
import { EabAccess } from "../../access";
import { HttpChallengeUploader } from "./uploads/api";
import { httpChallengeUploaderFactory } from "./uploads/factory.js";
export type { CertInfo };
@ -67,8 +66,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
],
},
required: true,
helper:
"DNS直接验证域名是在阿里云、腾讯云、华为云、Cloudflare、NameSilo、西数注册的选它。\nCNAME代理验证支持任何注册商注册的域名但第一次需要手动添加CNAME记录",
helper: `DNS直接验证域名是在阿里云、腾讯云、华为云、Cloudflare、NameSilo、西数注册的选它
CNAMECNAME
HTTP`,
})
challengeType!: string;
@ -127,75 +127,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
})
dnsProviderAccess!: number;
@TaskInput({
title: "文件上传方式",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "ftp", label: "FTP" },
{ value: "sftp", label: "SFTP" },
{ value: "alioss", label: "阿里云OSS" },
{ value: "tencentcos", label: "腾讯云COS" },
{ value: "qiniuoss", label: "七牛OSS" },
],
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.challengeType === 'http'
})
}
`,
required: true,
helper: "您的域名注册商或者域名的dns服务器属于哪个平台\n如果这里没有请选择CNAME代理验证校验方式",
})
httpUploadType!: string;
@TaskInput({
title: "文件上传授权",
component: {
name: "access-selector",
},
required: true,
helper: "请选择文件上传授权",
mergeScript: `return {
component:{
type: ctx.compute(({form})=>{
return form.httpUploadType
})
},
show: ctx.compute(({form})=>{
return form.challengeType === 'http'
})
}
`,
})
httpUploadAccess!: number;
@TaskInput({
title: "网站根路径",
component: {
name: "a-input",
},
required: true,
helper: "请选择网站根路径,校验文件将上传到此目录下",
mergeScript: `return {
show: ctx.compute(({form})=>{
return form.challengeType === 'http'
})
}
`,
})
httpUploadRootDir!: string;
@TaskInput({
title: "域名验证配置",
component: {
name: "domains-verify-plan-editor",
},
rules: [{ type: "checkCnameVerifyPlan" }],
rules: [{ type: "checkDomainVerifyPlan" }],
required: true,
helper: "如果选择CNAME方式请按照上面的显示给要申请证书的域名添加CNAME记录添加后点击验证验证成功后不要删除记录申请和续期证书会一直用它",
col: {
span: 24,
},
@ -203,10 +141,20 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
component:{
domains: ctx.compute(({form})=>{
return form.domains
}),
defaultType: ctx.compute(({form})=>{
return form.challengeType || 'cname'
})
},
show: ctx.compute(({form})=>{
return form.challengeType === 'cname'
return form.challengeType === 'cname' || form.challengeType === 'http'
}),
helper: ctx.compute(({form})=>{
if(form.challengeType === 'cname' ){
return '请按照上面的提示给要申请证书的域名添加CNAME记录添加后点击验证验证成功后不要删除记录申请和续期证书会一直用它'
}else if (form.challengeType === 'http'){
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录下'
}
})
}
`,
@ -393,15 +341,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
let dnsProvider: IDnsProvider = null;
let domainsVerifyPlan: DomainsVerifyPlan = null;
let httpUploader: HttpChallengeUploader = null;
if (this.challengeType === "cname") {
if (this.challengeType === "cname" || this.challengeType === "http") {
domainsVerifyPlan = await this.createDomainsVerifyPlan();
} else if (this.challengeType === "http") {
const access = await this.ctx.accessService.getById(this.httpUploadAccess);
httpUploader = await httpChallengeUploaderFactory.createUploaderByType(this.httpUploadType, {
rootDir: this.httpUploadRootDir,
access,
});
} else {
const dnsProviderType = this.dnsProviderType;
const access = await this.ctx.accessService.getById(this.dnsProviderAccess);
@ -414,7 +355,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
domains,
dnsProvider,
domainsVerifyPlan,
httpUploader,
csrInfo,
isTest: false,
privateKeyType: this.privateKeyType,
@ -449,10 +389,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const domainVerifyPlan = this.domainsVerifyPlan[domain];
let dnsProvider = null;
const cnameVerifyPlan: Record<string, CnameVerifyPlan> = {};
const httpVerifyPlan: Record<string, HttpVerifyPlan> = {};
if (domainVerifyPlan.type === "dns") {
const access = await this.ctx.accessService.getById(domainVerifyPlan.dnsProviderAccessId);
dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, access);
} else {
} else if (domainVerifyPlan.type === "cname") {
for (const key in domainVerifyPlan.cnameVerifyPlan) {
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key);
let dnsProvider = cnameRecord.commonDnsProvider;
@ -460,17 +401,39 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access);
}
cnameVerifyPlan[key] = {
type: "cname",
domain: cnameRecord.cnameProvider.domain,
fullRecord: cnameRecord.recordValue,
dnsProvider,
};
}
} else if (domainVerifyPlan.type === "http") {
const httpUploaderContext = {
accessService: this.ctx.accessService,
logger: this.logger,
utils,
};
for (const key in domainVerifyPlan.httpVerifyPlan) {
const httpRecord = domainVerifyPlan.httpVerifyPlan[key];
const access = await this.ctx.accessService.getById(httpRecord.httpUploaderAccess);
const httpUploader = await httpChallengeUploaderFactory.createUploaderByType(httpRecord.httpUploaderType, {
access,
rootDir: httpRecord.httpUploadRootDir,
ctx: httpUploaderContext,
});
httpVerifyPlan[key] = {
type: "http",
domain: key,
httpUploader,
};
}
}
plan[domain] = {
domain,
type: domainVerifyPlan.type,
dnsProvider,
cnameVerifyPlan,
httpVerifyPlan,
};
}
return plan;

View File

@ -1,24 +1,33 @@
import { HttpChallengeUploadContext } from "./api";
export class HttpChallengeUploaderFactory {
async getClassByType(type: string) {
if (type === "alioss") {
return (await import("./impls/alioss.js")).AliossHttpChallengeUploader;
const module = await import("./impls/alioss.js");
return module.AliossHttpChallengeUploader;
} else if (type === "ssh") {
return (await import("./impls/ssh.js")).SshHttpChallengeUploader;
const module = await import("./impls/ssh.js");
return module.SshHttpChallengeUploader;
} else if (type === "ftp") {
return (await import("./impls/ftp.js")).FtpHttpChallengeUploader;
const module = await import("./impls/ftp.js");
return module.FtpHttpChallengeUploader;
} else if (type === "tencentcos") {
return (await import("./impls/tencentcos.js")).TencentCosHttpChallengeUploader;
const module = await import("./impls/tencentcos.js");
return module.TencentCosHttpChallengeUploader;
} else if (type === "qiniuoss") {
return (await import("./impls/qiniuoss.js")).QiniuOssHttpChallengeUploader;
const module = await import("./impls/qiniuoss.js");
return module.QiniuOssHttpChallengeUploader;
} else {
throw new Error(`暂不支持此文件上传方式: ${type}`);
}
}
createUploaderByType(type: string, opts: { rootDir: string; access: any }) {
const cls = this.getClassByType(type);
async createUploaderByType(type: string, opts: { rootDir: string; access: any; ctx: HttpChallengeUploadContext }) {
const cls = await this.getClassByType(type);
if (cls) {
// @ts-ignore
return new cls(opts);
const instance = new cls(opts);
await instance.setCtx(opts.ctx);
return instance;
}
}
}

View File

@ -1,6 +1,6 @@
import { BaseHttpChallengeUploader } from "../api";
import { BaseHttpChallengeUploader } from "../api.js";
import { AliossAccess, AliyunAccess } from "@certd/plugin-lib";
import { AliossClient } from "@certd/plugin-lib/dist/aliyun/lib/oss-client";
import { AliossClient } from "@certd/plugin-lib";
export class AliossHttpChallengeUploader extends BaseHttpChallengeUploader<AliossAccess> {
async upload(filePath: string, fileContent: string) {

View File

@ -1,6 +1,5 @@
import { BaseHttpChallengeUploader } from "../api";
import { FtpAccess } from "@certd/plugin-lib";
import { FtpClient } from "@certd/plugin-lib/dist/ftp/client";
import { BaseHttpChallengeUploader } from "../api.js";
import { FtpAccess, FtpClient } from "@certd/plugin-lib";
export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> {
async upload(fileName: string, fileContent: string) {

View File

@ -1,7 +1,7 @@
import { BaseHttpChallengeUploader } from "../api";
import { FtpAccess } from "@certd/plugin-lib";
import { BaseHttpChallengeUploader } from "../api.js";
import { QiniuOssAccess } from "@certd/plugin-lib/dist/qiniu/access-oss";
export class QiniuOssHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> {
export class QiniuOssHttpChallengeUploader extends BaseHttpChallengeUploader<QiniuOssAccess> {
async upload(fileName: string, fileContent: string) {
return null;
}

View File

@ -1,4 +1,4 @@
import { BaseHttpChallengeUploader } from "../api";
import { BaseHttpChallengeUploader } from "../api.js";
import { SshAccess, SshClient } from "@certd/plugin-lib";
import path from "path";
import os from "os";

View File

@ -1,7 +1,7 @@
import { BaseHttpChallengeUploader } from "../api";
import { FtpAccess } from "@certd/plugin-lib";
import { BaseHttpChallengeUploader } from "../api.js";
import { TencentCosAccess } from "@certd/plugin-lib/dist/tencent/access-cos";
export class TencentCosHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> {
export class TencentCosHttpChallengeUploader extends BaseHttpChallengeUploader<TencentCosAccess> {
async upload(fileName: string, fileContent: string) {
return null;
}

View File

@ -1,2 +1,3 @@
export * from "./base-client.js";
export * from "./ssl-client.js";
export * from "./oss-client.js";

View File

@ -24,10 +24,7 @@ defineOptions({
name: "CnameVerifyPlan"
});
const emit = defineEmits<{
"update:modelValue": any;
change: Record<string, any>;
}>();
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: Record<string, any>;

View File

@ -0,0 +1,106 @@
<template>
<table class="http-verify-plan">
<thead>
<tr>
<td style="width: 160px">网站域名</td>
<td style="width: 100px; text-align: center">上传方式</td>
<td style="width: 150px">上传授权</td>
<td style="width: 200px">网站根目录路径</td>
</tr>
</thead>
<tbody v-if="records" class="http-record-body">
<template v-for="(item, key) of records" :key="key">
<tr>
<td class="domain">
{{ item.domain }}
</td>
<td>
<fs-dict-select v-model:value="item.httpUploaderType" :dict="uploaderTypeDict" @change="onRecordChange"></fs-dict-select>
</td>
<td>
<access-selector v-model="item.httpUploaderAccess" :type="item.httpUploaderType" @change="onRecordChange"></access-selector>
</td>
<td>
<a-input v-model:value="item.httpUploadRootDir" placeholder="网站根目录,如:/www/wwwroot" @change="onRecordChange"></a-input>
</td>
</tr>
</template>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from "vue";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
defineOptions({
name: "HttpVerifyPlan"
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: Record<string, any>;
}>();
const records: Ref<Record<string, HttpRecord>> = ref({});
watch(
() => {
return props.modelValue;
},
(value: any) => {
if (value) {
records.value = {
...value
};
}
},
{
immediate: true
}
);
function onRecordChange() {
emit("update:modelValue", records.value);
emit("change", records.value);
}
const uploaderTypeDict = dict({
data: [
{ label: "SFTP/SSH", value: "ssh" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" }
]
});
</script>
<style lang="less">
.http-verify-plan {
width: 100%;
table-layout: fixed;
tbody tr td {
border-top: 1px solid #e8e8e8 !important;
}
tr {
td {
border: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.center {
text-align: center;
}
}
//&:last-child {
// td {
// border-bottom: 0 !important;
// }
//}
}
}
</style>

View File

@ -13,7 +13,7 @@
<table class="plan-table">
<thead>
<tr>
<th>域名</th>
<th style="min-width: 100px">域名</th>
<th>验证方式</th>
<th>验证计划</th>
</tr>
@ -59,7 +59,7 @@
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
</div>
<div v-if="item.type === 'http'" class="plan-http">
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
</div>
</div>
</td>
@ -79,6 +79,7 @@ import { ref, watch } from "vue";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CnameVerifyPlan from "./cname-verify-plan.vue";
import HttpVerifyPlan from "./http-verify-plan.vue";
import psl from "psl";
import { Form } from "ant-design-vue";
import { DomainsVerifyPlanInput } from "./type";
@ -95,12 +96,17 @@ const challengeTypeOptions = ref<any[]>([
{
label: "CNAME验证",
value: "cname"
},
{
label: "HTTP验证",
value: "http"
}
]);
const props = defineProps<{
modelValue?: DomainsVerifyPlanInput;
domains?: string[];
defaultType?: string;
}>();
const emit = defineEmits<{
@ -132,6 +138,16 @@ function showError(error: string) {
type DomainGroup = Record<string, Record<string, CnameRecord>>;
watch(
() => {
return props.defaultType;
},
(value: string) => {
planRef.value = {};
onDomainsChanged(props.domains);
}
);
function onDomainsChanged(domains: string[]) {
console.log("域名变化", domains);
if (domains == null) {
@ -155,9 +171,7 @@ function onDomainsChanged(domains: string[]) {
group = {};
domainGroups[mainDomain] = group;
}
group[domain] = {
id: 0
};
group[domain] = {};
}
for (const domain in domainGroups) {
@ -166,27 +180,43 @@ function onDomainsChanged(domains: string[]) {
if (!planItem) {
planItem = {
domain,
type: "cname",
//@ts-ignore
type: props.defaultType || "cname",
//@ts-ignore
cnameVerifyPlan: {
...subDomains
},
//@ts-ignore
httpVerifyPlan: {
...subDomains
}
};
planRef.value[domain] = planItem;
} else {
const cnamePlan = planItem.cnameVerifyPlan;
for (const subDomain in subDomains) {
if (!cnamePlan[subDomain]) {
//@ts-ignore
cnamePlan[subDomain] = {
id: 0
};
}
}
const cnamePlan = planItem.cnameVerifyPlan;
for (const subDomain in subDomains) {
//@ts-ignore
cnamePlan[subDomain] = {
id: 0
};
}
for (const subDomain of Object.keys(cnamePlan)) {
if (!subDomains[subDomain]) {
delete cnamePlan[subDomain];
}
for (const subDomain of Object.keys(cnamePlan)) {
if (!subDomains[subDomain]) {
delete cnamePlan[subDomain];
}
}
// httpVerifyPlan
const httpPlan = planItem.httpVerifyPlan;
for (const subDomain in subDomains) {
//@ts-ignore
httpPlan[subDomain] = {
domain: subDomain
};
}
for (const subDomain of Object.keys(httpPlan)) {
if (!subDomains[subDomain]) {
delete httpPlan[subDomain];
}
}
}

View File

@ -1,12 +1,13 @@
import Validator from "async-validator";
import { DomainsVerifyPlanInput } from "./type";
function checkCnameVerifyPlan(rule, value: DomainsVerifyPlanInput) {
function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
if (value == null) {
return true;
}
for (const domain in value) {
if (value[domain].type === "cname") {
const type = value[domain].type;
if (type === "cname") {
const subDomains = Object.keys(value[domain].cnameVerifyPlan);
if (subDomains.length > 0) {
for (const subDomain of subDomains) {
@ -16,7 +17,21 @@ function checkCnameVerifyPlan(rule, value: DomainsVerifyPlanInput) {
}
}
}
} else {
} else if (type === "http") {
const subDomains = Object.keys(value[domain].httpVerifyPlan);
if (subDomains.length > 0) {
for (const subDomain of subDomains) {
const plan = value[domain].httpVerifyPlan[subDomain];
if (plan.httpUploaderType == null) {
throw new Error(`域名${subDomain}的上传方式必须填写`);
} else if (plan.httpUploaderAccess == null) {
throw new Error(`域名${subDomain}的上传授权信息必须填写`);
} else if (plan.httpUploadRootDir == null) {
throw new Error(`域名${subDomain}的网站根路径必须填写`);
}
}
}
} else if (type === "dns") {
if (value[domain].dnsProviderType == null || value[domain].dnsProviderAccessId == null) {
throw new Error(`DNS模式下域名${domain}的DNS类型和授权信息必须填写`);
}
@ -25,4 +40,4 @@ function checkCnameVerifyPlan(rule, value: DomainsVerifyPlanInput) {
return true;
}
// 注册自定义验证器
Validator.register("checkCnameVerifyPlan", checkCnameVerifyPlan);
Validator.register("checkDomainVerifyPlan", checkDomainVerifyPlan);