feat: 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析

This commit is contained in:
xiaojunnuo
2024-10-07 03:21:16 +08:00
parent 0c8e83e125
commit f3d35084ed
123 changed files with 2373 additions and 456 deletions
@@ -0,0 +1,27 @@
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController, Constants } from '@certd/lib-server';
import { CnameRecordService } from '../service/cname-record-service.js';
import { CnameProviderService } from '../../sys/cname/service/cname-provider-service.js';
/**
* 授权
*/
@Provide()
@Controller('/api/cname/provider')
export class CnameProviderController extends BaseController {
@Inject()
service: CnameRecordService;
@Inject()
providerService: CnameProviderService;
getService(): CnameRecordService {
return this.service;
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.userId = this.ctx.user.id;
const res = await this.providerService.find({});
return this.ok(res);
}
}
@@ -0,0 +1,61 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import { CnameRecordService } from '../service/cname-record-service.js';
/**
* 授权
*/
@Provide()
@Controller('/api/cname/record')
export class CnameRecordController extends CrudController<CnameRecordService> {
@Inject()
service: CnameRecordService;
getService(): CnameRecordService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.ctx.user.id;
return await super.page(body);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.userId = this.ctx.user.id;
return super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.ctx.user.id;
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean: any) {
await this.service.checkUserId(bean.id, this.ctx.user.id);
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.delete(id);
}
@Post('/getByDomain', { summary: Constants.per.authOnly })
async getByDomain(@Body(ALL) body: { domain: string; createOnNotFound: boolean }) {
const userId = this.ctx.user.id;
const res = await this.service.getByDomain(body.domain, userId, body.createOnNotFound);
return this.ok(res);
}
}
@@ -0,0 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
* cname record配置
*/
@Entity('cd_cname_record')
export class CnameRecordEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: '用户ID', name: 'user_id' })
userId: number;
@Column({ comment: '证书申请域名', length: 100 })
domain: string;
@Column({ comment: '主机记录', name: 'host_record', length: 100 })
hostRecord: string;
@Column({ comment: '记录值', name: 'record_value', length: 200 })
recordValue: string;
@Column({ comment: 'CNAME提供者', name: 'cname_provider_id' })
cnameProviderId: number;
@Column({ comment: '验证状态', length: 20 })
status: string;
@Column({
comment: '创建时间',
name: 'create_time',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
comment: '修改时间',
name: 'update_time',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}
@@ -0,0 +1,133 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService, ValidateException } from '@certd/lib-server';
import { CnameRecordEntity } from '../entity/cname-record.js';
import { v4 as uuidv4 } from 'uuid';
import { CnameProviderService } from '../../sys/cname/service/cname-provider-service.js';
import { CnameProviderEntity } from '../../sys/cname/entity/cname_provider.js';
import { parseDomain } from '@certd/plugin-cert';
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CnameRecordService extends BaseService<CnameRecordEntity> {
@InjectEntityModel(CnameRecordEntity)
repository: Repository<CnameRecordEntity>;
@Inject()
cnameProviderService: CnameProviderService;
getRepository() {
return this.repository;
}
/**
* 新增
* @param param 数据
*/
async add(param: any): Promise<CnameRecordEntity> {
if (!param.domain) {
throw new ValidateException('域名不能为空');
}
if (!param.userId) {
throw new ValidateException('userId不能为空');
}
if (param.domain.startsWith('*.')) {
param.domain = param.domain.substring(2);
}
const info = await this.getRepository().findOne({ where: { domain: param.domain } });
if (info) {
return info;
}
let cnameProvider: CnameProviderEntity = null;
if (!param.cnameProviderId) {
//获取默认的cnameProviderId
cnameProvider = await this.cnameProviderService.getByPriority();
if (cnameProvider == null) {
throw new ValidateException('找不到CNAME服务,请先联系管理员添加CNAME服务');
}
} else {
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
}
this.cnameProviderChanged(param, cnameProvider);
param.status = 'cname';
const { id } = await super.add(param);
return await this.info(id);
}
private cnameProviderChanged(param: any, cnameProvider: CnameProviderEntity) {
param.cnameProviderId = cnameProvider.id;
const realDomain = parseDomain(param.domain);
const prefix = param.domain.replace(realDomain, '');
let hostRecord = `_acme-challenge.${prefix}`;
if (hostRecord.endsWith('.')) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
}
param.hostRecord = hostRecord;
const cnameKey = uuidv4().replaceAll('-', '');
param.recordValue = `${cnameKey}.${cnameProvider.domain}`;
}
async update(param: any) {
if (!param.id) {
throw new ValidateException('id不能为空');
}
const old = await this.info(param.id);
if (!old) {
throw new ValidateException('数据不存在');
}
if (old.domain !== param.domain) {
throw new ValidateException('域名不允许修改');
}
if (old.cnameProviderId !== param.cnameProviderId) {
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
this.cnameProviderChanged(param, cnameProvider);
param.status = 'cname';
}
return await super.update(param);
}
async validate(id: number) {
const info = await this.info(id);
if (info.status === 'success') {
return true;
}
//开始校验
// 1. dnsProvider
// 2. 添加txt记录
// 3. 检查原域名是否有cname记录
}
async getByDomain(domain: string, userId: number, createOnNotFound = true) {
if (!domain) {
throw new ValidateException('domain不能为空');
}
if (userId == null) {
throw new ValidateException('userId不能为空');
}
let record = await this.getRepository().findOne({ where: { domain, userId } });
if (record == null) {
if (createOnNotFound) {
record = await this.add({ domain, userId });
} else {
throw new ValidateException(`找不到${domain}的CNAME记录`);
}
}
const provider = await this.cnameProviderService.info(record.cnameProviderId);
if (provider == null) {
throw new ValidateException(`找不到${domain}的CNAME服务`);
}
return {
...record,
cnameProvider: provider,
};
}
}