pref: 支持子域名托管的域名证书申请

pull/370/head
xiaojunnuo 2025-04-11 12:13:57 +08:00
parent f68af7dcf2
commit 67f956d4a0
21 changed files with 700 additions and 130 deletions

View File

@ -4,5 +4,5 @@ export * from "./config.js";
export * from "./url.js";
export * from "./emit.js";
export type IServiceGetter = {
get: (name: string) => Promise<any>;
get: <T>(name: string) => Promise<T>;
};

View File

@ -217,4 +217,20 @@ export abstract class BaseService<T> {
}
throw new PermissionException('权限不足');
}
async batchDelete(ids: number[], userId: number) {
if(userId >0){
const list = await this.getRepository().find({
where: {
// @ts-ignore
id: In(ids),
userId,
},
})
// @ts-ignore
ids = list.map(item => item.id)
}
await this.delete(ids);
}
}

View File

@ -27,6 +27,7 @@ export type DnsProviderContext = {
logger: ILogger;
http: HttpClient;
utils: typeof utils;
domainParser: IDomainParser;
};
export interface IDnsProvider<T = any> {
@ -35,3 +36,11 @@ export interface IDnsProvider<T = any> {
removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
setCtx(ctx: DnsProviderContext): void;
}
export interface ISubDomainsGetter {
getSubDomains(): Promise<string[]>;
}
export interface IDomainParser {
parse(fullDomain: string): Promise<string>;
}

View File

@ -1,6 +1,4 @@
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js";
//@ts-ignore
import psl from "psl";
import { dnsProviderRegistry } from "./registry.js";
import { Decorator } from "@certd/pipeline";
import { HttpClient, ILogger } from "@certd/basic";
@ -16,6 +14,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
this.http = ctx.http;
}
async parseDomain(fullDomain: string) {
return await this.ctx.domainParser.parse(fullDomain);
}
abstract createRecord(options: CreateRecordOptions): Promise<T>;
abstract onInstance(): Promise<void>;
@ -23,14 +25,6 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
}
export function parseDomain(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed.domain as string;
}
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {
const { dnsProviderType, context } = opts;
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);

View File

@ -0,0 +1,32 @@
import { IDomainParser, ISubDomainsGetter } from "./api";
//@ts-ignore
import psl from "psl";
export class DomainParser implements IDomainParser {
subDomainsGetter: ISubDomainsGetter;
constructor(subDomainsGetter: ISubDomainsGetter) {
this.subDomainsGetter = subDomainsGetter;
}
parseDomain(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed.domain as string;
}
async parse(fullDomain: string) {
const subDomains = await this.subDomainsGetter.getSubDomains();
if (subDomains && subDomains.length > 0) {
for (const subDomain of subDomains) {
if (fullDomain.endsWith(subDomain)) {
//找到子域名托管
return subDomain;
}
}
}
return this.parseDomain(fullDomain);
}
}

View File

@ -5,8 +5,9 @@ import * as _ from "lodash-es";
import { Challenge } from "@certd/acme-client/types/rfc8555";
import { IContext } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
import { IDnsProvider, parseDomain } from "../../dns-provider/index.js";
import { IDnsProvider, IDomainParser } from "../../dns-provider/index.js";
import { HttpChallengeUploader } from "./uploads/api.js";
export type CnameVerifyPlan = {
type?: string;
domain: string;
@ -61,6 +62,8 @@ type AcmeServiceOptions = {
privateKeyType?: PrivateKeyType;
signal?: AbortSignal;
maxCheckRetryCount?: number;
userId: number;
domainParser: IDomainParser;
};
export class AcmeService {
@ -174,7 +177,7 @@ export class AcmeService {
this.logger.info("Triggered challengeCreateFn()");
const fullDomain = authz.identifier.value;
let domain = parseDomain(fullDomain);
let domain = await this.options.domainParser.parse(fullDomain);
this.logger.info("主域名为:" + domain);
const getChallenge = (type: string) => {
@ -240,7 +243,7 @@ export class AcmeService {
const cname = cnameVerifyPlan[fullDomain];
if (cname) {
dnsProvider = cname.dnsProvider;
domain = parseDomain(cname.domain);
domain = await this.options.domainParser.parse(cname.domain);
fullRecord = cname.fullRecord;
}
} else {

View File

@ -4,12 +4,13 @@ import { utils } from "@certd/basic";
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";
import { createDnsProvider, DnsProviderContext, IDnsProvider, ISubDomainsGetter } from "../../dns-provider/index.js";
import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
import { GoogleClient } from "../../libs/google.js";
import { EabAccess } from "../../access";
import { httpChallengeUploaderFactory } from "./uploads/factory.js";
import { DomainParser } from "../../dns-provider/domain-parser.js";
export * from "./base.js";
export type { CertInfo };
export * from "./cert-reader.js";
@ -314,7 +315,10 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
}
}
this.eab = eab;
const subDomainsGetter = await this.ctx.serviceGetter.get<ISubDomainsGetter>("subDomainsGetter");
const domainParser = new DomainParser(subDomainsGetter);
this.acme = new AcmeService({
userId: this.ctx.user.id,
userContext: this.userContext,
logger: this.logger,
sslProvider: this.sslProvider,
@ -325,8 +329,7 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
privateKeyType: this.privateKeyType,
signal: this.ctx.signal,
maxCheckRetryCount: this.maxCheckRetryCount,
// cnameProxyService: this.ctx.cnameProxyService,
// dnsProviderCreator: this.createDnsProvider.bind(this),
domainParser,
});
}
@ -387,7 +390,8 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
}
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils };
const domainParser = this.acme.options.domainParser;
const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils, domainParser };
return await createDnsProvider({
dnsProviderType,
context,

View File

@ -1,6 +1,7 @@
import { request } from "/src/api/service";
const apiPrefix = "/cname/record";
const subDomainApiPrefix = "/pi/subDomain";
export type CnameRecord = {
id?: number;
@ -18,7 +19,7 @@ export type DomainGroupItem = {
export async function GetList() {
return await request({
url: apiPrefix + "/list",
method: "post"
method: "post",
});
}
@ -28,8 +29,8 @@ export async function GetByDomain(domain: string) {
method: "post",
data: {
domain,
createOnNotFound: true
}
createOnNotFound: true,
},
});
}
@ -38,7 +39,17 @@ export async function DoVerify(id: number) {
url: apiPrefix + "/verify",
method: "post",
data: {
id
}
id,
},
});
}
export async function ParseDomain(fullDomain: string) {
return await request({
url: subDomainApiPrefix + "/parseDomain",
method: "post",
data: {
fullDomain,
},
});
}

View File

@ -32,26 +32,14 @@
<div class="form-item">
<span class="label">DNS类型</span>
<span class="input">
<fs-dict-select
v-model:value="item.dnsProviderType"
size="small"
:dict="dnsProviderTypeDict"
placeholder="DNS提供商"
@change="onPlanChanged"
></fs-dict-select>
<fs-dict-select v-model:value="item.dnsProviderType" size="small" :dict="dnsProviderTypeDict" placeholder="DNS提供商" @change="onPlanChanged"></fs-dict-select>
</span>
</div>
<a-divider type="vertical" />
<div class="form-item">
<span class="label">DNS授权</span>
<span class="input">
<access-selector
v-model="item.dnsProviderAccessId"
size="small"
:type="item.dnsProviderType"
placeholder="请选择"
@change="onPlanChanged"
></access-selector>
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderType" placeholder="请选择" @change="onPlanChanged"></access-selector>
</span>
</div>
</div>
@ -80,28 +68,27 @@ 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";
//@ts-ignore
import psl from "psl";
import { Form } from "ant-design-vue";
import { DomainsVerifyPlanInput } from "./type";
import { CnameRecord, DomainGroupItem } from "./api";
import { DomainGroupItem, ParseDomain } from "./api";
defineOptions({
name: "DomainsVerifyPlanEditor"
name: "DomainsVerifyPlanEditor",
});
const challengeTypeOptions = ref<any[]>([
{
label: "DNS验证",
value: "dns"
value: "dns",
},
{
label: "CNAME验证",
value: "cname"
value: "cname",
},
{
label: "HTTP验证",
value: "http"
}
value: "http",
},
]);
const props = defineProps<{
@ -122,7 +109,7 @@ function fullscreenExit() {
}
const planRef = ref<DomainsVerifyPlanInput>(props.modelValue || {});
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict"
url: "pi/dnsProvider/dnsProviderTypeDict",
});
const formItemContext = Form.useInjectFormItemContext();
@ -139,7 +126,7 @@ function showError(error: string) {
type DomainGroup = Record<string, DomainGroupItem>;
function onDomainsChanged(domains: string[]) {
async function onDomainsChanged(domains: string[]) {
if (domains == null) {
return;
}
@ -147,12 +134,7 @@ function onDomainsChanged(domains: string[]) {
const domainGroups: DomainGroup = {};
for (let domain of domains) {
const keyDomain = domain.replace("*.", "");
const parsed = psl.parse(keyDomain);
if (parsed.error) {
showError(`域名${domain}解析失败: ${JSON.stringify(parsed.error)}`);
continue;
}
const mainDomain = parsed.domain;
const mainDomain = await ParseDomain(keyDomain);
if (mainDomain == null) {
continue;
}
@ -161,7 +143,7 @@ function onDomainsChanged(domains: string[]) {
group = {
domain: mainDomain,
domains: [],
keySubDomains: []
keySubDomains: [],
} as DomainGroupItem;
domainGroups[mainDomain] = group;
}
@ -180,7 +162,7 @@ function onDomainsChanged(domains: string[]) {
//@ts-ignore
cnameVerifyPlan: {},
//@ts-ignore
httpVerifyPlan: {}
httpVerifyPlan: {},
};
planRef.value[domain] = planItem;
}
@ -196,7 +178,7 @@ function onDomainsChanged(domains: string[]) {
if (!cnameOrigin[subDomain]) {
//@ts-ignore
planItem.cnameVerifyPlan[subDomain] = {
id: 0
id: 0,
};
} else {
planItem.cnameVerifyPlan[subDomain] = cnameOrigin[subDomain];
@ -205,14 +187,14 @@ function onDomainsChanged(domains: string[]) {
if (!cnamePlan[subDomain]) {
//@ts-ignore
cnamePlan[subDomain] = {
id: 0
id: 0,
};
}
if (!httpOrigin[subDomain]) {
//@ts-ignore
planItem.httpVerifyPlan[subDomain] = {
domain: subDomain
domain: subDomain,
};
} else {
planItem.httpVerifyPlan[subDomain] = httpOrigin[subDomain];
@ -221,7 +203,7 @@ function onDomainsChanged(domains: string[]) {
if (!httpPlan[subDomain]) {
//@ts-ignore
httpPlan[subDomain] = {
domain: subDomain
domain: subDomain,
};
}
}
@ -255,7 +237,7 @@ watch(
},
{
immediate: true,
deep: true
deep: true,
}
);
</script>

View File

@ -98,6 +98,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "子域名托管设置",
name: "SubDomain",
path: "/certd/pipeline/subDomain",
component: "/certd/pipeline/sub-domain/index.vue",
meta: {
icon: "ion:link-outline",
auth: true,
keepAlive: true,
},
},
{
title: "流水线分组管理",
name: "PipelineGroupManager",

View File

@ -0,0 +1,60 @@
// @ts-ignore
import { request } from "/src/api/service";
const apiPrefix = "/pi/subDomain";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/batchDelete",
method: "post",
data: { ids },
});
}

View File

@ -0,0 +1,123 @@
import * as api from "./api";
import { Ref, ref } from "vue";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
// tabs: {
// name: "status",
// show: true,
// },
rowHandle: {
minWidth: 200,
fixed: "right",
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 80,
},
form: {
show: false,
},
},
domain: {
title: "子域名",
type: "text",
search: {
show: true,
},
editForm: {
component: {
disabled: true,
},
},
},
disabled: {
title: "是否禁用",
type: "dict-switch",
dict: dict({
data: [
{ value: false, label: "启用", color: "green" },
{ value: true, label: "禁用", color: "gray" },
],
}),
search: {
show: true,
},
form: {
value: false,
},
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false,
},
column: {
show: true,
},
},
},
},
};
}

View File

@ -0,0 +1,57 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
子域名托管
<span class="sub"> 当你的域名设置了子域名托管需要在此处创建记录否则申请证书将失败 </span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
defineOptions({
name: "CnameRecord",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error("请先勾选记录");
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>

View File

@ -1,3 +1,19 @@
ALTER TABLE pi_plugin ADD COLUMN "pluginType" varchar(100);
ALTER TABLE pi_plugin ADD COLUMN "metadata" varchar(40960);
ALTER TABLE pi_plugin ADD COLUMN "author" varchar(100);
CREATE TABLE "pi_sub_domain"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer,
"domain" varchar(100),
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_sub_domain_user_id" ON "pi_sub_domain" ("user_id");
CREATE INDEX "index_sub_domain_domain" ON "pi_sub_domain" ("domain");

View File

@ -1,7 +1,8 @@
import {ALL, Body, Controller, Inject, Post, Provide} from '@midwayjs/core';
import {AccessGetter, AccessService, BaseController, Constants} from '@certd/lib-server';
import {AccessService, BaseController, Constants} from '@certd/lib-server';
import {
AccessRequestHandleReq,
IAccessService,
ITaskPlugin,
newAccess,
newNotification,
@ -13,6 +14,7 @@ import {
import {EmailService} from '../../../modules/basic/service/email-service.js';
import {http, HttpRequestConfig, logger, mergeUtils, utils} from '@certd/basic';
import {NotificationService} from '../../../modules/pipeline/service/notification-service.js';
import {TaskServiceBuilder} from "../../../modules/pipeline/service/task-service-getter.js";
@Provide()
@Controller('/api/pi/handle')
@ -23,6 +25,8 @@ export class HandleController extends BaseController {
@Inject()
emailService: EmailService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
@Inject()
notificationService: NotificationService;
@ -82,8 +86,6 @@ export class HandleController extends BaseController {
//@ts-ignore
const instance = plugin as ITaskPlugin;
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const download = async (config: HttpRequestConfig, savePath: string) => {
await utils.download({
http,
@ -93,13 +95,9 @@ export class HandleController extends BaseController {
});
};
const serviceContainer:any = {
}
const serviceGetter = {
get:(name: string) => {
return serviceContainer[name]
}
}
const taskServiceGetter = this.taskServiceBuilder.create({userId})
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService")
//@ts-ignore
const taskCtx: TaskInstanceContext = {
pipeline: undefined,
@ -125,7 +123,7 @@ export class HandleController extends BaseController {
// }),
// signal: this.abort.signal,
utils,
serviceGetter
serviceGetter:taskServiceGetter
};
instance.setCtx(taskCtx);
mergeUtils.merge(plugin, body.input);

View File

@ -0,0 +1,81 @@
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
import {Constants, CrudController} from '@certd/lib-server';
import {SubDomainService, SubDomainsGetter} from "../../../modules/pipeline/service/sub-domain-service.js";
import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js';
/**
*
*/
@Provide()
@Controller('/api/pi/subDomain')
export class SubDomainController extends CrudController<SubDomainService> {
@Inject()
service: SubDomainService;
getService() {
return this.service;
}
@Post('/parseDomain', { summary: Constants.per.authOnly })
async parseDomain(@Body("fullDomain") fullDomain:string) {
const userId = this.getUserId()
const subDomainGetter = new SubDomainsGetter(userId, this.service)
const domainParser = new DomainParser(subDomainGetter)
const domain = await domainParser.parse(fullDomain)
return this.ok(domain);
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body) {
body.query = body.query ?? {};
delete body.query.userId;
const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
};
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery,
});
return this.ok(res);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
return super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) {
bean.userId = this.getUserId();
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
}
@Post('/batchDelete', { summary: Constants.per.authOnly })
async batchDelete(@Body('ids') ids: number[]) {
await this.service.batchDelete(ids, this.getUserId());
return this.ok({});
}
}

View File

@ -1,18 +1,17 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService, PlusService, ValidateException } from '@certd/lib-server';
import { CnameRecordEntity, CnameRecordStatusType } from '../entity/cname-record.js';
import { createDnsProvider, IDnsProvider, parseDomain } from '@certd/plugin-cert';
import { CnameProvider, CnameRecord } from '@certd/pipeline';
import { cache, http, logger, utils } from '@certd/basic';
import { AccessService } from '@certd/lib-server';
import { isDev } from '@certd/basic';
import { walkTxtRecord } from '@certd/acme-client';
import { CnameProviderService } from './cname-provider-service.js';
import { CnameProviderEntity } from '../entity/cname-provider.js';
import { CommonDnsProvider } from './common-provider.js';
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import {AccessService, BaseService, PlusService, ValidateException} from '@certd/lib-server';
import {CnameRecordEntity, CnameRecordStatusType} from '../entity/cname-record.js';
import {createDnsProvider, IDnsProvider} from '@certd/plugin-cert';
import {CnameProvider, CnameRecord} from '@certd/pipeline';
import {cache, http, isDev, logger, utils} from '@certd/basic';
import {walkTxtRecord} from '@certd/acme-client';
import {CnameProviderService} from './cname-provider-service.js';
import {CnameProviderEntity} from '../entity/cname-provider.js';
import {CommonDnsProvider} from './common-provider.js';
import {SubDomainService, SubDomainsGetter} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
type CnameCheckCacheValue = {
validating: boolean;
@ -40,6 +39,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
@Inject()
plusService: PlusService;
@Inject()
subDomainService: SubDomainService;
//@ts-ignore
getRepository() {
return this.repository;
@ -74,17 +76,20 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} else {
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
}
this.cnameProviderChanged(param, cnameProvider);
await this.cnameProviderChanged(param.userId,param, cnameProvider);
param.status = 'cname';
const { id } = await super.add(param);
return await this.info(id);
}
private cnameProviderChanged(param: any, cnameProvider: CnameProviderEntity) {
private async cnameProviderChanged(userId:number,param: any, cnameProvider: CnameProviderEntity) {
param.cnameProviderId = cnameProvider.id;
const realDomain = parseDomain(param.domain);
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter);
const realDomain = await domainParser.parse(param.domain);
const prefix = param.domain.replace(realDomain, '');
let hostRecord = `_acme-challenge.${prefix}`;
if (hostRecord.endsWith('.')) {
@ -111,7 +116,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
if (old.cnameProviderId !== param.cnameProviderId) {
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
this.cnameProviderChanged(param, cnameProvider);
await this.cnameProviderChanged(old.userId,param, cnameProvider);
param.status = 'cname';
}
return await super.update(param);
@ -185,6 +190,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return true;
}
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter);
const cacheKey = `cname.record.verify.${bean.id}`;
let value: CnameCheckCacheValue = cache.get(cacheKey);
@ -219,7 +227,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
const context = { access, logger, http, utils };
const context = { access, logger, http, utils,domainParser };
const dnsProvider: IDnsProvider = await createDnsProvider({
dnsProviderType: cnameProvider.dnsProviderType,
context,
@ -239,7 +247,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return false;
}
const originDomain = parseDomain(bean.domain);
const originDomain = await domainParser.parse(bean.domain);
const fullDomain = `${bean.hostRecord}.${originDomain}`;
logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
@ -285,7 +294,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
ttl: ttl,
});
const domain = parseDomain(bean.recordValue);
const domain = await domainParser.parse(bean.recordValue);
const fullRecord = bean.recordValue;
const hostRecord = fullRecord.replace(`.${domain}`, '');
const req = {

View File

@ -0,0 +1,32 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
*
*/
@Entity('pi_sub_domain')
export class SubDomainEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: 'UserId' })
userId: number;
@Column({ name: 'domain', comment: '子域名' })
domain: string;
@Column({ name: 'disabled', comment: '禁用' })
disabled: boolean;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}

View File

@ -1,8 +1,7 @@
import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, MoreThan, Repository } from "typeorm";
import {Config, Inject, Provide, Scope, ScopeEnum, sleep} from "@midwayjs/core";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {In, MoreThan, Repository} from "typeorm";
import {
AccessGetter,
AccessService,
BaseService,
NeedSuiteException,
@ -12,30 +11,40 @@ import {
SysSettingsService,
SysSiteInfo
} from "@certd/lib-server";
import { PipelineEntity } from "../entity/pipeline.js";
import { PipelineDetail } from "../entity/vo/pipeline-detail.js";
import { Executor, Pipeline, ResultType, RunHistory, RunnableCollection, SysInfo, UserInfo } from "@certd/pipeline";
import { DbStorage } from "./db-storage.js";
import { StorageService } from "./storage-service.js";
import { Cron } from "../../cron/cron.js";
import { HistoryService } from "./history-service.js";
import { HistoryEntity } from "../entity/history.js";
import { HistoryLogEntity } from "../entity/history-log.js";
import { HistoryLogService } from "./history-log-service.js";
import { EmailService } from "../../basic/service/email-service.js";
import { UserService } from "../../sys/authority/service/user-service.js";
import { CnameRecordService } from "../../cname/service/cname-record-service.js";
import { CnameProxyService } from "./cname-proxy-service.js";
import { PluginConfigGetter } from "../../plugin/service/plugin-config-getter.js";
import {PipelineEntity} from "../entity/pipeline.js";
import {PipelineDetail} from "../entity/vo/pipeline-detail.js";
import {
Executor,
IAccessService,
ICnameProxyService,
INotificationService,
Pipeline,
ResultType,
RunHistory,
RunnableCollection,
SysInfo,
UserInfo
} from "@certd/pipeline";
import {DbStorage} from "./db-storage.js";
import {StorageService} from "./storage-service.js";
import {Cron} from "../../cron/cron.js";
import {HistoryService} from "./history-service.js";
import {HistoryEntity} from "../entity/history.js";
import {HistoryLogEntity} from "../entity/history-log.js";
import {HistoryLogService} from "./history-log-service.js";
import {EmailService} from "../../basic/service/email-service.js";
import {UserService} from "../../sys/authority/service/user-service.js";
import {CnameRecordService} from "../../cname/service/cname-record-service.js";
import {PluginConfigGetter} from "../../plugin/service/plugin-config-getter.js";
import dayjs from "dayjs";
import { DbAdapter } from "../../db/index.js";
import { isComm } from "@certd/plus-core";
import { logger } from "@certd/basic";
import { UrlService } from "./url-service.js";
import { NotificationService } from "./notification-service.js";
import { NotificationGetter } from "./notification-getter.js";
import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core";
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
import {DbAdapter} from "../../db/index.js";
import {isComm} from "@certd/plus-core";
import {logger} from "@certd/basic";
import {UrlService} from "./url-service.js";
import {NotificationService} from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js";
const runningTasks: Map<string | number, Executor> = new Map();
@ -65,6 +74,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
pluginConfigGetter: PluginConfigGetter;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
@Inject()
sysSettingsService: SysSettingsService;
@ -473,20 +485,19 @@ export class PipelineService extends BaseService<PipelineEntity> {
role: userIsAdmin ? 'admin' : 'user',
};
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getWithAccessByDomain.bind(this.cnameRecordService));
const notificationGetter = new NotificationGetter(userId, this.notificationService);
const sysInfo: SysInfo = {};
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
sysInfo.title = siteInfo.title;
}
const serviceContainer = {}
const serviceGetter = {
get:(name: string) => {
return serviceContainer[name]
}
}
const taskServiceGetter = this.taskServiceBuilder.create({
userId,
})
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService")
const notificationGetter =await taskServiceGetter.get<INotificationService>("notificationService")
const cnameProxyService =await taskServiceGetter.get<ICnameProxyService>("cnameProxyService")
const executor = new Executor({
user,
pipeline,
@ -500,7 +511,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
notificationService: notificationGetter,
fileRootDir: this.certdConfig.fileRootDir,
sysInfo,
serviceGetter
serviceGetter:taskServiceGetter
});
try {
runningTasks.set(historyId, executor);

View File

@ -0,0 +1,58 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {BaseService, SysSettingsService} from '@certd/lib-server';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import {SubDomainEntity} from '../entity/sub-domain.js';
import {EmailService} from '../../basic/service/email-service.js';
import {ISubDomainsGetter} from "@certd/plugin-cert";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SubDomainService extends BaseService<SubDomainEntity> {
@InjectEntityModel(SubDomainEntity)
repository: Repository<SubDomainEntity>;
@Inject()
emailService: EmailService;
@Inject()
sysSettingsService: SysSettingsService;
//@ts-ignore
getRepository() {
return this.repository;
}
async getListByUserId(userId:number):Promise<string[]>{
if (!userId) {
return [];
}
const list = await this.find({
where: {
userId,
disabled: false,
},
});
return list.map(item=>item.domain);
}
}
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
subDomainService: SubDomainService;
constructor(userId: number, subDomainService: SubDomainService) {
this.userId = userId;
this.subDomainService = subDomainService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId)
}
}

View File

@ -0,0 +1,63 @@
import {IServiceGetter} from "@certd/pipeline";
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {SubDomainService, SubDomainsGetter} from "./sub-domain-service.js";
import {AccessGetter, AccessService} from "@certd/lib-server";
import {CnameProxyService} from "./cname-proxy-service.js";
import {NotificationGetter} from "./notification-getter.js";
import {NotificationService} from "./notification-service.js";
import {CnameRecordService} from "../../cname/service/cname-record-service.js";
export class TaskServiceGetter implements IServiceGetter{
serviceContainer:Record<string, any>;
constructor(serviceContainer:Record<string, any>) {
this.serviceContainer = serviceContainer;
}
async get<T>(serviceName: string): Promise<T> {
const ret = this.serviceContainer[serviceName] as T;
if(!ret){
throw new Error(`service ${serviceName} not found`)
}
return ret
}
}
export type TaskServiceCreateReq = {
userId: number;
}
export type TaskServiceContainer = {
subDomainsGetter:SubDomainsGetter;
accessService: AccessGetter;
cnameProxyService: CnameProxyService;
notificationService: NotificationGetter;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
@Inject()
subDomainService: SubDomainService;
@Inject()
accessService: AccessService;
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
notificationService: NotificationService;
create(req:TaskServiceCreateReq){
const userId = req.userId;
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getWithAccessByDomain.bind(this.cnameRecordService));
const notificationGetter = new NotificationGetter(userId, this.notificationService);
const serviceContainer:TaskServiceContainer = {
subDomainsGetter:new SubDomainsGetter(req.userId, this.subDomainService),
accessService: accessGetter,
cnameProxyService:cnameProxyService,
notificationService:notificationGetter
}
return new TaskServiceGetter(serviceContainer)
}
}