Files
certd/packages/ui/certd-server/src/modules/cert/service/domain-service.ts
T

411 lines
12 KiB
TypeScript
Raw Normal View History

2026-01-16 18:18:39 +08:00
import { http, logger, utils } from '@certd/basic';
2026-01-22 00:59:28 +08:00
import { AccessService, BaseService } from '@certd/lib-server';
import { doPageTurn, Pager, PageRes } from '@certd/pipeline';
import { DomainVerifiers } from "@certd/plugin-cert";
import { createDnsProvider, DomainParser, parseDomainByPsl } from "@certd/plugin-lib";
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import dayjs from 'dayjs';
import { In, Not, Repository } from 'typeorm';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
2026-01-16 18:18:39 +08:00
import { TaskServiceBuilder } from '../../pipeline/service/getter/task-service-getter.js';
2026-01-22 00:59:28 +08:00
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { DomainEntity } from '../entity/domain.js';
2026-01-24 23:36:44 +08:00
import { BackTask, taskExecutor } from '../../basic/service/task-executor.js';
2025-07-09 16:00:55 +08:00
2026-01-22 00:59:28 +08:00
export interface SyncFromProviderReq {
2026-01-16 18:18:39 +08:00
userId: number;
dnsProviderType: string;
dnsProviderAccessId: string;
}
2025-07-09 16:00:55 +08:00
2026-01-22 00:59:28 +08:00
2025-07-09 16:00:55 +08:00
/**
*
*/
@Provide()
2026-01-22 00:59:28 +08:00
@Scope(ScopeEnum.Request, { allowDowngrade: true })
2025-07-09 16:00:55 +08:00
export class DomainService extends BaseService<DomainEntity> {
@InjectEntityModel(DomainEntity)
repository: Repository<DomainEntity>;
@Inject()
accessService: AccessService;
@Inject()
subDomainService: SubDomainService;
2025-07-09 16:00:55 +08:00
2025-07-13 23:08:00 +08:00
@Inject()
cnameRecordService: CnameRecordService;
2026-01-16 18:18:39 +08:00
@Inject()
taskServiceBuilder: TaskServiceBuilder;
2025-07-09 16:00:55 +08:00
//@ts-ignore
getRepository() {
return this.repository;
}
2025-07-10 17:00:47 +08:00
async add(param) {
2026-01-22 00:59:28 +08:00
if (param.userId == null) {
2025-07-10 17:00:47 +08:00
throw new Error('userId 不能为空');
}
if (!param.domain) {
throw new Error('domain 不能为空');
}
const old = await this.repository.findOne({
where: {
domain: param.domain,
userId: param.userId
}
});
if (old) {
throw new Error(`域名(${param.domain})不能重复`);
}
2026-01-21 18:24:03 +08:00
if (!param.fromType) {
param.fromType = 'manual'
}
2025-07-10 17:00:47 +08:00
return await super.add(param);
}
async update(param) {
if (!param.id) {
throw new Error('id 不能为空');
}
const old = await this.info(param.id)
if (!old) {
throw new Error('domain记录不存在');
}
const same = await this.repository.findOne({
where: {
domain: param.domain,
userId: old.userId,
id: Not(param.id)
}
});
if (same) {
throw new Error(`域名(${param.domain})不能重复`);
}
delete param.userId
return await super.update(param);
}
/**
*
* @param userId
* @param domains //去除* 且去重之后的域名列表
*/
2026-01-22 00:59:28 +08:00
async getDomainVerifiers(userId: number, domains: string[]): Promise<DomainVerifiers> {
2026-01-22 00:59:28 +08:00
const mainDomainMap: Record<string, string> = {}
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const mainDomains = []
for (const domain of domains) {
const mainDomain = await domainParser.parse(domain);
mainDomainMap[domain] = mainDomain;
mainDomains.push(mainDomain)
}
//匹配DNS记录
2026-01-22 00:59:28 +08:00
let allDomains = [...domains, ...mainDomains]
//去重
allDomains = [...new Set(allDomains)]
2025-07-13 23:08:00 +08:00
//从 domain 表中获取配置
const domainRecords = await this.find({
where: {
domain: In(allDomains),
2025-07-13 23:08:00 +08:00
userId,
2026-01-22 00:59:28 +08:00
disabled: false,
}
})
2026-01-22 00:59:28 +08:00
const dnsMap = domainRecords.filter(item => item.challengeType === 'dns').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
2025-07-13 23:08:00 +08:00
2026-01-22 00:59:28 +08:00
const httpMap = domainRecords.filter(item => item.challengeType === 'http').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
2025-07-13 23:08:00 +08:00
//从cname record表中获取配置
const cnameRecords = await this.cnameRecordService.find({
where: {
domain: In(allDomains),
userId,
status: "valid",
}
})
const cnameMap = cnameRecords.reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
2025-07-13 23:08:00 +08:00
//构建域名验证计划
2026-01-22 00:59:28 +08:00
const domainVerifiers: DomainVerifiers = {}
for (const domain of domains) {
const mainDomain = mainDomainMap[domain]
const dnsRecord = dnsMap[mainDomain]
if (dnsRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'dns',
dns: {
dnsProviderType: dnsRecord.dnsProviderType,
2025-07-13 23:08:00 +08:00
dnsProviderAccessId: dnsRecord.dnsProviderAccess
}
}
continue
}
2026-01-22 00:59:28 +08:00
const cnameRecord: CnameRecordEntity = cnameMap[domain]
if (cnameRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'cname',
cname: {
2025-07-13 23:08:00 +08:00
domain: cnameRecord.domain,
hostRecord: cnameRecord.hostRecord,
recordValue: cnameRecord.recordValue
}
}
continue
}
2025-07-13 23:58:07 +08:00
const httpRecord = httpMap[domain]
if (httpRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'http',
http: {
httpUploaderType: httpRecord.httpUploaderType,
httpUploaderAccess: httpRecord.httpUploaderAccess,
httpUploadRootDir: httpRecord.httpUploadRootDir
}
}
2026-01-22 00:59:28 +08:00
continue
}
domainVerifiers[domain] = null
}
return domainVerifiers;
}
2026-01-16 18:18:39 +08:00
2026-01-22 00:59:28 +08:00
async doSyncFromProvider(req: SyncFromProviderReq) {
2026-01-24 23:36:44 +08:00
const key = `user_${req.userId || 0}`
2026-01-22 00:59:28 +08:00
taskExecutor.start('syncFromProviderTask', new BackTask({
2026-01-24 23:36:44 +08:00
key,
title: `从域名提供商导入域名(${key})`,
2026-01-22 00:59:28 +08:00
run: async (task: BackTask) => {
await this._syncFromProvider(req, task)
},
}))
}
private async _syncFromProvider(req: SyncFromProviderReq, task: BackTask) {
2026-01-16 18:18:39 +08:00
const { userId, dnsProviderType, dnsProviderAccessId } = req;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const serviceGetter = this.taskServiceBuilder.create({ userId });
const access = await this.accessService.getById(dnsProviderAccessId, userId);
const context = { access, logger, http, utils, domainParser, serviceGetter };
// 翻页查询dns的记录
2026-01-22 00:59:28 +08:00
const dnsProvider = await createDnsProvider({ dnsProviderType, context })
2026-01-16 18:18:39 +08:00
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const challengeType = "dns"
2026-01-22 11:55:01 +08:00
const getPage = async (pager: Pager) => {
return await dnsProvider.getDomainListPage(pager)
}
const itemHandle = async (domainRecord: any) => {
2026-01-22 00:59:28 +08:00
task.incrementCurrent()
2026-01-16 18:18:39 +08:00
const domain = domainRecord.domain
2026-01-21 18:24:03 +08:00
2026-01-16 18:18:39 +08:00
const old = await this.findOne({
where: {
domain,
userId,
}
})
if (old) {
2026-01-23 15:28:33 +08:00
// if (old.fromType !== 'auto') {
// //如果是手动的,跳过更新校验配置
// return
// }
if (old) {
//如果old存在,直接跳过
2026-01-24 23:36:44 +08:00
task.incrementSkip()
2026-01-22 00:59:28 +08:00
return
2026-01-20 00:13:05 +08:00
}
2026-01-22 00:59:28 +08:00
const updateObj: any = {
id: old.id,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
2026-01-20 00:13:05 +08:00
}
2026-01-16 18:18:39 +08:00
//更新
2026-01-21 18:24:03 +08:00
await super.update(updateObj)
2026-01-16 18:18:39 +08:00
} else {
//添加
await this.add({
userId,
domain,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
2026-01-20 00:13:05 +08:00
disabled: false,
2026-01-23 15:28:33 +08:00
fromType: 'manual',
2026-01-16 18:18:39 +08:00
})
2026-01-23 15:28:33 +08:00
logger.info(`导入域名${domain}到用户${userId}`)
2026-01-16 18:18:39 +08:00
}
}
2026-01-22 00:59:28 +08:00
const batchHandle = async (pageRes: PageRes<any>) => {
2026-01-24 23:36:44 +08:00
task.setTotal(pageRes.total || 0)
2026-01-22 00:59:28 +08:00
}
2026-01-24 23:36:44 +08:00
await doPageTurn({ pager, getPage, itemHandle, batchHandle })
const key = `user_${userId || 0}`
logger.info(`从域名提供商${dnsProviderType}导入域名完成(${key}),共导入${task.current}个域名,跳过${task.getSkipCount()}个域名`)
2026-01-22 00:59:28 +08:00
}
async doSyncDomainsExpirationDate(req: { userId?: number }) {
const userId = req.userId
2026-01-24 23:36:44 +08:00
const key = `user_${userId || 0}`
2026-01-22 00:59:28 +08:00
taskExecutor.start('syncDomainsExpirationDateTask', new BackTask({
2026-01-24 23:36:44 +08:00
key,
title: `同步注册域名过期时间(${key}))`,
2026-01-22 00:59:28 +08:00
run: async (task: BackTask) => {
await this._syncDomainsExpirationDate({ userId, task })
}
}))
}
private async _syncDomainsExpirationDate(req: { userId?: number, task: BackTask }) {
//同步所有域名的过期时间
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const dnsJson = await http.request({
url: "https://data.iana.org/rdap/dns.json",
method: "GET",
})
const rdapMap: Record<string, string> = {}
for (const item of dnsJson.services) {
// [["store","work"], ["https://rdap.centralnic.com/store/"]],
const suffixes = item[0]
const urls = item[1]
for (const suffix of suffixes) {
rdapMap[suffix] = urls[0]
}
}
const getDomainExpirationDate = async (domain: string) => {
const parsed = parseDomainByPsl(domain)
const mainDomain = parsed.domain || ''
if (mainDomain !== domain) {
2026-01-24 23:36:44 +08:00
req.task.addError(`${domain}】为子域名,跳过同步`)
2026-01-22 00:59:28 +08:00
return
}
const suffix = parsed.tld || ''
const rdapUrl = rdapMap[suffix]
if (!rdapUrl) {
2026-01-24 23:36:44 +08:00
req.task.addError(`${domain}】未找到${suffix}的rdap地址`)
return
2026-01-22 00:59:28 +08:00
}
// https://rdap.nic.work/domain/handsfree.work
const rdap = await http.request({
url: `${rdapUrl}domain/${domain}`,
method: "GET",
})
let res: any = {}
const events = rdap.events || []
for (const item of events) {
if (item.eventAction === 'expiration') {
res.expirationDate = dayjs(item.eventDate).valueOf()
} else if (item.eventAction === 'registration') {
res.registrationDate = dayjs(item.eventDate).valueOf()
2026-01-16 18:18:39 +08:00
}
2026-01-22 00:59:28 +08:00
}
return res
}
const query: any = {
challengeType: "dns",
}
2026-01-24 23:36:44 +08:00
if (req.userId != null) {
2026-01-22 00:59:28 +08:00
query.userId = req.userId
}
const getDomainPage = async (pager: Pager) => {
const pageRes = await this.page({
query: query,
// buildQuery(bq) {
// bq.andWhere(" (expiration_date is null or expiration_date < :now) ", { now: dayjs().add(1, 'month').valueOf() })
// },
2026-01-22 00:59:28 +08:00
page: {
offset: pager.getOffset(),
limit: pager.pageSize,
2026-01-16 18:18:39 +08:00
}
2026-01-22 00:59:28 +08:00
})
req.task.total = pageRes.total
return {
list: pageRes.records,
total: pageRes.total,
}
}
2026-01-16 18:18:39 +08:00
2026-01-22 00:59:28 +08:00
const itemHandle = async (item: any) => {
req.task.incrementCurrent()
try {
const res = await getDomainExpirationDate(item.domain)
if (!res) {
return
2026-01-16 18:18:39 +08:00
}
2026-01-22 00:59:28 +08:00
const { expirationDate, registrationDate } = res
if (!expirationDate) {
2026-01-24 23:36:44 +08:00
req.task.addError(`${item.domain}】获取域名${item.domain}过期时间失败`)
2026-01-22 00:59:28 +08:00
return
}
2026-01-24 23:36:44 +08:00
logger.info(`${item.domain}】更新域名过期时间:${dayjs(expirationDate).format('YYYY-MM-DD')}`)
2026-01-22 00:59:28 +08:00
const updateObj: any = {
id: item.id,
expirationDate: expirationDate,
registrationDate: registrationDate,
}
//更新
await super.update(updateObj)
} catch (error) {
2026-01-24 23:36:44 +08:00
const errorMsg = `${item.domain}${error.message ?? error}`
req.task.addError(errorMsg)
2026-01-22 00:59:28 +08:00
} finally {
await utils.sleep(1000)
2026-01-16 18:18:39 +08:00
}
}
2026-01-22 00:59:28 +08:00
await doPageTurn({ pager, getPage: getDomainPage, itemHandle: itemHandle })
2026-01-24 23:36:44 +08:00
const key = `user_${req.userId || 'all'}`
logger.info(`同步用户(${key})注册域名过期时间完成(${req.task.getSuccessCount()}个成功,${req.task.errors.length}个失败)` )
2026-01-16 18:18:39 +08:00
}
2025-07-09 16:00:55 +08:00
}