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

This commit is contained in:
xiaojunnuo
2025-04-11 12:13:57 +08:00
parent f68af7dcf2
commit 67f956d4a0
21 changed files with 700 additions and 130 deletions

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)
}
}