Merge remote-tracking branch 'origin/v2-dev' into v2-dev

This commit is contained in:
xiaojunnuo
2025-10-11 17:00:00 +08:00
167 changed files with 3871 additions and 1223 deletions
@@ -1,7 +1,6 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { AccessService, Constants } from '@certd/lib-server';
import { AccessController } from '../../user/pipeline/access-controller.js';
import { checkComm } from '@certd/plus-core';
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { AccessService, Constants } from "@certd/lib-server";
import { AccessController } from "../../user/pipeline/access-controller.js";
/**
* 授权
@@ -17,7 +16,7 @@ export class SysAccessController extends AccessController {
}
getUserId() {
checkComm();
// checkComm();
return 0;
}
@@ -0,0 +1,47 @@
import { BaseController } from '@certd/lib-server';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { NetTestService } from '../../../modules/sys/nettest/nettest-service.js';
@Provide()
@Controller('/api/sys/nettest/')
export class SysNetTestController extends BaseController {
@Inject()
netTestService: NetTestService;
@Post('/domainResolve', { summary: 'sys:settings:view' })
public async domainResolve(@Body(ALL) body: { domain: string }) {
const { domain } = body;
const result = await this.netTestService.domainResolve(domain);
return this.ok(result);
}
// ping
@Post('/ping', { summary: 'sys:settings:view' })
public async ping(@Body(ALL) body: { domain: string }) {
const { domain } = body;
const result = await this.netTestService.ping(domain);
return this.ok(result);
}
// telnet
@Post('/telnet', { summary: 'sys:settings:view' })
public async telnet(@Body(ALL) body: { domain: string, port: number }) {
const { domain, port } = body;
const result = await this.netTestService.telnet(domain, port);
return this.ok(result);
}
// telnet
@Post('/serverInfo', { summary: 'sys:settings:view' })
public async serverInfo() {
const result = await this.netTestService.serverInfo();
return this.ok(result);
}
}
@@ -192,4 +192,11 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
await this.service.saveSetting(blankSetting);
return this.ok({});
}
@Post("/captchaTest", { summary: "sys:settings:edit" })
async captchaTest(@Body(ALL) body: any) {
await this.codeService.checkCaptcha(body)
return this.ok({});
}
}
@@ -11,56 +11,59 @@ import {
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
import { checkPlus } from "@certd/plus-core";
import { http, logger, utils } from "@certd/basic";
import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/task-service-getter.js";
/**
* Addon
*/
@Provide()
@Controller('/api/addon')
@Controller("/api/addon")
export class AddonController extends CrudController<AddonService> {
@Inject()
service: AddonService;
@Inject()
authService: AuthService;
@Inject()
taskServiceBuilder:TaskServiceBuilder
getService(): AddonService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
@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() });
qb.andWhere("user_id = :userId", { userId: this.getUserId() });
};
const res = await this.service.page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery,
buildQuery
});
return this.ok(res);
}
@Post('/list', { summary: Constants.per.authOnly })
@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 })
@Post("/add", { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) {
bean.userId = this.getUserId();
const type = bean.type;
const addonType = bean.addonType;
if (! type || !addonType){
throw new ValidateException('请选择Addon类型');
if (!type || !addonType) {
throw new ValidateException("请选择Addon类型");
}
const define: AddonDefine = this.service.getDefineByType(type,addonType);
const define: AddonDefine = this.service.getDefineByType(type, addonType);
if (!define) {
throw new ValidateException('Addon类型不存在');
throw new ValidateException("Addon类型不存在");
}
if (define.needPlus) {
checkPlus();
@@ -68,19 +71,19 @@ export class AddonController extends CrudController<AddonService> {
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
@Post("/update", { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
const old = await this.service.info(bean.id);
if (!old) {
throw new ValidateException('Addon配置不存在');
throw new ValidateException("Addon配置不存在");
}
if (old.type !== bean.type ) {
if (old.type !== bean.type) {
const addonType = old.type;
const type = bean.type;
const define: AddonDefine = this.service.getDefineByType(type,addonType);
const define: AddonDefine = this.service.getDefineByType(type, addonType);
if (!define) {
throw new ValidateException('Addon类型不存在');
throw new ValidateException("Addon类型不存在");
}
if (define.needPlus) {
checkPlus();
@@ -89,26 +92,27 @@ export class AddonController extends CrudController<AddonService> {
delete bean.userId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
@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) {
@Post("/delete", { summary: Constants.per.authOnly })
async delete(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
}
@Post('/define', { summary: Constants.per.authOnly })
async define(@Query('type') type: string,@Query('addonType') addonType: string) {
const notification = this.service.getDefineByType(type,addonType);
@Post("/define", { summary: Constants.per.authOnly })
async define(@Query("type") type: string, @Query("addonType") addonType: string) {
const notification = this.service.getDefineByType(type, addonType);
return this.ok(notification);
}
@Post('/getTypeDict', { summary: Constants.per.authOnly })
async getTypeDict(@Query('addonType') addonType: string) {
@Post("/getTypeDict", { summary: Constants.per.authOnly })
async getTypeDict(@Query("addonType") addonType: string) {
const list: any = this.service.getDefineList(addonType);
let dict = [];
for (const item of list) {
@@ -116,7 +120,7 @@ export class AddonController extends CrudController<AddonService> {
value: item.name,
label: item.title,
needPlus: item.needPlus ?? false,
icon: item.icon,
icon: item.icon
});
}
dict = dict.sort(a => {
@@ -125,13 +129,13 @@ export class AddonController extends CrudController<AddonService> {
return this.ok(dict);
}
@Post('/simpleInfo', { summary: Constants.per.authOnly })
async simpleInfo(@Query('addonType') addonType: string,@Query('id') id: number) {
@Post("/simpleInfo", { summary: Constants.per.authOnly })
async simpleInfo(@Query("addonType") addonType: string, @Query("id") id: number) {
if (id === 0) {
//获取默认
const res = await this.service.getDefault(this.getUserId(),addonType);
const res = await this.service.getDefault(this.getUserId(), addonType);
if (!res) {
throw new ValidateException('默认Addon配置不存在');
throw new ValidateException("默认Addon配置不存在");
}
const simple = await this.service.getSimpleInfo(res.id);
return this.ok(simple);
@@ -141,27 +145,27 @@ export class AddonController extends CrudController<AddonService> {
return this.ok(res);
}
@Post('/getDefaultId', { summary: Constants.per.authOnly })
async getDefaultId(@Query('addonType') addonType: string) {
const res = await this.service.getDefault(this.getUserId(),addonType);
@Post("/getDefaultId", { summary: Constants.per.authOnly })
async getDefaultId(@Query("addonType") addonType: string) {
const res = await this.service.getDefault(this.getUserId(), addonType);
return this.ok(res?.id);
}
@Post('/setDefault', { summary: Constants.per.authOnly })
async setDefault(@Query('addonType') addonType: string,@Query('id') id: number) {
@Post("/setDefault", { summary: Constants.per.authOnly })
async setDefault(@Query("addonType") addonType: string, @Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
const res = await this.service.setDefault(id, this.getUserId(),addonType);
const res = await this.service.setDefault(id, this.getUserId(), addonType);
return this.ok(res);
}
@Post('/options', { summary: Constants.per.authOnly })
async options(@Query('addonType') addonType: string) {
@Post("/options", { summary: Constants.per.authOnly })
async options(@Query("addonType") addonType: string) {
const res = await this.service.list({
query: {
userId: this.getUserId(),
addonType
},
}
});
for (const item of res) {
delete item.setting;
@@ -170,7 +174,7 @@ export class AddonController extends CrudController<AddonService> {
}
@Post('/handle', { summary: Constants.per.authOnly })
@Post("/handle", { summary: Constants.per.authOnly })
async handle(@Body(ALL) body: AddonRequestHandleReq) {
const userId = this.getUserId();
let inputAddon = body.input.addon;
@@ -178,21 +182,24 @@ export class AddonController extends CrudController<AddonService> {
const oldEntity = await this.service.info(body.input.id);
if (oldEntity) {
if (oldEntity.userId !== userId) {
throw new Error('addon not found');
throw new Error("addon not found");
}
// const param: any = {
// type: body.typeName,
// setting: JSON.stringify(body.input.access),
// };
inputAddon = JSON.parse( oldEntity.setting)
inputAddon = JSON.parse(oldEntity.setting);
}
}
const serviceGetter = this.taskServiceBuilder.create({ userId });
const ctx = {
http: http,
logger:logger,
utils:utils,
}
const addon = await newAddon(body.addonType,body.typeName, inputAddon,ctx);
logger: logger,
utils: utils,
serviceGetter
};
const addon = await newAddon(body.addonType, body.typeName, inputAddon, ctx);
const res = await addon.onRequest(body);
return this.ok(res);
}
@@ -85,10 +85,18 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
}
@Post('/verify', { summary: Constants.per.authOnly })
async verify(@Body(ALL) body: { id: string }) {
async verify(@Body(ALL) body: { id: number }) {
const userId = this.getUserId();
await this.service.checkUserId(body.id, userId);
const res = await this.service.verify(body.id);
return this.ok(res);
}
@Post('/resetStatus', { summary: Constants.per.authOnly })
async resetStatus(@Body(ALL) body: { id: number }) {
const userId = this.getUserId();
await this.service.checkUserId(body.id, userId);
const res = await this.service.resetStatus(body.id);
return this.ok(res);
}
}
@@ -1,7 +1,7 @@
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
import {Constants, CrudController} from '@certd/lib-server';
import {SubDomainService} from "../../../modules/pipeline/service/sub-domain-service.js";
import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js';
import {DomainParser} from '@certd/plugin-cert';
import { SubDomainsGetter } from '../../../modules/pipeline/service/getter/sub-domain-getter.js';
/**
@@ -1,6 +1,6 @@
import { Autoload, Config, Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
import { CommonException } from '@certd/lib-server';
import { CommonException, SysSettingsService } from "@certd/lib-server";
import { UserService } from '../../modules/sys/authority/service/user-service.js';
import { logger } from '@certd/basic';
import {UserSettingsService} from "../../modules/mine/service/user-settings-service.js";
@@ -17,6 +17,8 @@ export class ResetPasswdMiddleware implements IWebMiddleware {
@Inject()
userSettingsService: UserSettingsService;
@Inject()
sysSettingsService: SysSettingsService;
@Config('system.resetAdminPasswd')
private resetAdminPasswd: boolean;
@@ -40,8 +42,12 @@ export class ResetPasswdMiddleware implements IWebMiddleware {
userId: 1,
key:"user.two.factor"
})
const publicSettings = await this.sysSettingsService.getPublicSettings()
publicSettings.captchaEnabled = false
await this.sysSettingsService.savePublicSettings(publicSettings);
const user = await this.userService.info(1);
logger.info(`重置1号管理员用户的密码完成,2FA设置已删除,用户名:${user.username},新密码:${newPasswd},请在登录进去之后尽快修改密码`);
logger.info(`重置1号管理员用户的密码完成,2FA设置已删除,验证码登录已禁用,用户名:${user.username},新密码:${newPasswd},请在登录进去之后尽快修改密码`);
}
}
}
@@ -2,7 +2,7 @@ import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { CertInfoService } from "../monitor/index.js";
import { pipelineEmitter } from "@certd/pipeline";
import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert";
import { PipelineEvent } from "@certd/pipeline/dist/service/emit.js";
import { PipelineEvent } from "@certd/pipeline";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -1,7 +1,8 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { AddonService, SysSettingsService } from "@certd/lib-server";
import { SysSettingsService } from "@certd/lib-server";
import { logger } from "@certd/basic";
import { ICaptchaAddon } from "../../../plugins/plugin-captcha/api.js";
import { AddonGetterService } from "../../pipeline/service/addon-getter-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -9,45 +10,48 @@ export class CaptchaService {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
addonService: AddonService;
addonGetterService: AddonGetterService;
async getCaptcha(captchaAddonId?:number){
async getCaptcha(captchaAddonId?: number) {
if (!captchaAddonId) {
const settings = await this.sysSettingsService.getPublicSettings()
captchaAddonId = settings.captchaAddonId ?? 0
const settings = await this.sysSettingsService.getPublicSettings();
captchaAddonId = settings.captchaAddonId ?? 0;
}
const addon:ICaptchaAddon = await this.addonService.getAddonById(captchaAddonId,true,0)
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0, {
type: "captcha",
name: "image"
});
if (!addon) {
throw new Error('验证码插件还未配置')
throw new Error("验证码插件还未配置");
}
return await addon.getCaptcha()
return await addon.getCaptcha();
}
async doValidate(opts:{form:any,must?:boolean,captchaAddonId?:number}){
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number }) {
if (!opts.captchaAddonId) {
const settings = await this.sysSettingsService.getPublicSettings()
opts.captchaAddonId = settings.captchaAddonId ?? 0
const settings = await this.sysSettingsService.getPublicSettings();
opts.captchaAddonId = settings.captchaAddonId ?? 0;
}
const addon = await this.addonService.getById(opts.captchaAddonId,0)
const addon = await this.addonGetterService.getById(opts.captchaAddonId, 0);
if (!addon) {
if (opts.must) {
throw new Error('请先配置验证码插件');
throw new Error("请先配置验证码插件");
}
logger.warn('验证码插件还未配置,忽略验证码校验')
return true
logger.warn("验证码插件还未配置,忽略验证码校验");
return true;
}
if (!opts.form) {
throw new Error('请输入验证码');
throw new Error("请输入验证码");
}
const res = await addon.onValidate(opts.form)
const res = await addon.onValidate(opts.form);
if (!res) {
throw new Error('验证码错误');
throw new Error("验证码错误");
}
return true
return true;
}
@@ -3,7 +3,7 @@ import { cache, isDev, randomNumber } from '@certd/basic';
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { SmsServiceFactory } from '../sms/factory.js';
import { ISmsService } from '../sms/api.js';
import { CodeErrorException } from '@certd/lib-server/dist/basic/exception/code-error-exception.js';
import { CodeErrorException } from '@certd/lib-server';
import { EmailService } from './email-service.js';
import { AccessService } from '@certd/lib-server';
import { AccessSysGetter } from '@certd/lib-server';
@@ -4,7 +4,7 @@ import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import {DomainParser} from "@certd/plugin-cert";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
@@ -13,6 +13,8 @@ export class CnameRecordEntity {
@Column({ comment: '证书申请域名', length: 100 })
domain: string;
@Column({ comment: '主域名', name: 'main_domain', length: 100 })
mainDomain:string;
@Column({ comment: '主机记录', name: 'host_record', length: 100 })
hostRecord: string;
@@ -17,10 +17,11 @@ import { getAuthoritativeDnsResolver, 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 { DomainParser } from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import { DomainParser } from "@certd/plugin-cert";
import punycode from "punycode.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
type CnameCheckCacheValue = {
validating: boolean;
@@ -36,7 +37,7 @@ type CnameCheckCacheValue = {
* 授权
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CnameRecordService extends BaseService<CnameRecordEntity> {
@InjectEntityModel(CnameRecordEntity)
repository: Repository<CnameRecordEntity>;
@@ -55,6 +56,10 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
@Inject()
subDomainService: SubDomainService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
//@ts-ignore
getRepository() {
return this.repository;
@@ -66,16 +71,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
*/
async add(param: any): Promise<CnameRecordEntity> {
if (!param.domain) {
throw new ValidateException('域名不能为空');
throw new ValidateException("域名不能为空");
}
if (!param.userId) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
if (param.domain.startsWith('*.')) {
if (param.domain.startsWith("*.")) {
param.domain = param.domain.substring(2);
}
param.domain = param.domain.trim()
const info = await this.getRepository().findOne({where: {domain: param.domain, userId: param.userId}});
param.domain = param.domain.trim();
const info = await this.getRepository().findOne({ where: { domain: param.domain, userId: param.userId } });
if (info) {
return info;
}
@@ -85,63 +90,62 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//获取默认的cnameProviderId
cnameProvider = await this.cnameProviderService.getByPriority();
if (cnameProvider == null) {
throw new ValidateException('找不到CNAME服务,请先前往“系统管理->CNAME服务设置”添加CNAME服务');
throw new ValidateException("找不到CNAME服务,请先前往“系统管理->CNAME服务设置”添加CNAME服务");
}
} else {
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
}
await this.cnameProviderChanged(param.userId, param, cnameProvider);
param.status = 'cname';
const {id} = await super.add(param);
param.status = "cname";
const { id } = await super.add(param);
return await this.info(id);
}
private async cnameProviderChanged(userId: number, param: any, cnameProvider: CnameProviderEntity) {
param.cnameProviderId = cnameProvider.id;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
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, '');
const prefix = param.domain.replace(realDomain, "");
let hostRecord = `_acme-challenge.${prefix}`;
if (hostRecord.endsWith('.')) {
if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
}
param.hostRecord = hostRecord;
param.mainDomain = realDomain;
const randomKey = utils.id.simpleNanoId(6).toLowerCase();
let userIdHash = ""
if(param.cnameProviderId < 0){
//公共cname服务
userIdHash = utils.hash.md5(`userId${userId}_${randomKey}`).substring(0, 10)
}else{
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo)
userIdHash = utils.hash.md5(`${installInfo.siteId}_${randomKey}`).substring(0, 10)
}
const cnameKey = `${userIdHash}-${randomKey}`;
const safeDomain = param.domain.replaceAll('.', '-');
const userIdHex = utils.hash.toHex(userId);
let userKeyHash = "";
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`;
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10);
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`);
const cnameKey = `${userKeyHash}-${userIdHex}-${randomKey}`;
const safeDomain = param.domain.replaceAll(".", "-");
param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`;
}
async update(param: any) {
if (!param.id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
const old = await this.info(param.id);
if (!old) {
throw new ValidateException('数据不存在');
throw new ValidateException("数据不存在");
}
if (old.domain !== param.domain) {
throw new ValidateException('域名不允许修改');
if (param.domain && old.domain !== param.domain) {
throw new ValidateException("域名不允许修改");
}
if (old.cnameProviderId !== param.cnameProviderId) {
if (param.cnameProviderId && old.cnameProviderId !== param.cnameProviderId) {
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
await this.cnameProviderChanged(old.userId, param, cnameProvider);
param.status = 'cname';
param.status = "cname";
}
return await super.update(param);
}
@@ -166,7 +170,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} else {
record.commonDnsProvider = new CommonDnsProvider({
config: record.cnameProvider,
plusService: this.plusService,
plusService: this.plusService
});
}
@@ -175,19 +179,22 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
async getByDomain(domain: string, userId: number, createOnNotFound = true) {
if (!domain) {
throw new ValidateException('domain不能为空');
throw new ValidateException("domain不能为空");
}
if (userId == null) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
let record = await this.getRepository().findOne({where: {domain, userId}});
let record = await this.getRepository().findOne({ where: { domain, userId } });
if (record == null) {
if (createOnNotFound) {
record = await this.add({domain, userId});
record = await this.add({ domain, userId });
} else {
throw new ValidateException(`找不到${domain}的CNAME记录`);
}
}
await this.fillMainDomain(record);
const provider = await this.cnameProviderService.info(record.cnameProviderId);
if (provider == null) {
throw new ValidateException(`找不到${domain}的CNAME服务`);
@@ -196,25 +203,53 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return {
...record,
cnameProvider: {
...provider,
} as CnameProvider,
...provider
} as CnameProvider
} as CnameRecord;
}
async fillMainDomain(record: CnameRecordEntity, update = true) {
const notMainDomain = !record.mainDomain;
const hasErrorMainDomain = record.mainDomain && !record.mainDomain.includes(".");
if (notMainDomain || hasErrorMainDomain) {
let domainPrefix = record.hostRecord.replace("_acme-challenge", "");
if (domainPrefix.startsWith(".")) {
domainPrefix = domainPrefix.substring(1);
}
if (domainPrefix) {
const prefixStr = domainPrefix + ".";
record.mainDomain = record.domain.substring(prefixStr.length);
}else{
record.mainDomain = record.domain;
}
if (update) {
await this.update({
id: record.id,
mainDomain: record.mainDomain
});
}
}
}
/**
* 验证是否配置好cname
* @param id
*/
async verify(id: string) {
async verify(id: number) {
const bean = await this.info(id);
if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`);
}
if (bean.status === 'valid') {
if (bean.status === "valid") {
return true;
}
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService)
await this.getByDomain(bean.domain, bean.userId);
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter);
const cacheKey = `cname.record.verify.${bean.id}`;
@@ -224,7 +259,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
value = {
validating: false,
pass: false,
startTime: new Date().getTime(),
startTime: new Date().getTime()
};
}
let ttl = 5 * 60 * 1000;
@@ -246,15 +281,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//公共CNAME
return new CommonDnsProvider({
config: cnameProvider,
plusService: this.plusService,
plusService: this.plusService
});
}
const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
const context = {access, logger, http, utils, domainParser};
const context = { access, logger, http, utils, domainParser, serviceGetter };
const dnsProvider: IDnsProvider = await createDnsProvider({
dnsProviderType: cnameProvider.dnsProviderType,
context,
context
});
return dnsProvider;
};
@@ -262,15 +298,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
const clearVerifyRecord = async () => {
cache.delete(cacheKey);
try {
let dnsProvider = value.dnsProvider
let dnsProvider = value.dnsProvider;
if (!dnsProvider) {
dnsProvider = await buildDnsProvider();
}
await dnsProvider.removeRecord({
recordReq: value.recordReq,
recordRes: value.recordRes,
recordRes: value.recordRes
});
logger.info('删除CNAME的校验DNS记录成功');
logger.info("删除CNAME的校验DNS记录成功");
} catch (e) {
logger.error(`删除CNAME的校验DNS记录失败, ${e.message}req:${JSON.stringify(value.recordReq)}recordRes:${JSON.stringify(value.recordRes)}`, e);
}
@@ -283,8 +319,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (value.startTime + ttl < new Date().getTime()) {
logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`);
clearInterval(value.intervalId);
await this.updateStatus(bean.id, 'timeout');
await clearVerifyRecord()
await this.updateStatus(bean.id, "timeout");
await clearVerifyRecord();
return false;
}
@@ -295,7 +331,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
//检查是否有重复的acme配置
await this.checkRepeatAcmeChallengeRecords(fullDomain,bean.recordValue)
await this.checkRepeatAcmeChallengeRecords(fullDomain, bean.recordValue);
// const txtRecords = await dns.promises.resolveTxt(fullDomain);
// if (txtRecords.length) {
@@ -312,9 +348,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (success) {
clearInterval(value.intervalId);
logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`);
await this.updateStatus(bean.id, 'valid', "");
await this.updateStatus(bean.id, "valid", "");
value.pass = true;
await clearVerifyRecord()
await clearVerifyRecord();
return success;
}
};
@@ -325,88 +361,88 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
cache.set(cacheKey, value, {
ttl: ttl,
ttl: ttl
});
const domain = await domainParser.parse(bean.recordValue);
const fullRecord = bean.recordValue;
const hostRecord = fullRecord.replace(`.${domain}`, '');
const hostRecord = fullRecord.replace(`.${domain}`, "");
const req = {
domain: domain,
fullRecord: fullRecord,
hostRecord: hostRecord,
type: 'TXT',
value: testRecordValue,
type: "TXT",
value: testRecordValue
};
const dnsProvider = await buildDnsProvider();
if(dnsProvider.usePunyCode()){
if (dnsProvider.usePunyCode()) {
//是否需要中文转英文
req.domain = dnsProvider.punyCodeEncode(req.domain)
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord)
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord)
req.value = dnsProvider.punyCodeEncode(req.value)
req.domain = dnsProvider.punyCodeEncode(req.domain);
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord);
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord);
req.value = dnsProvider.punyCodeEncode(req.value);
}
const recordRes = await dnsProvider.createRecord(req);
value.dnsProvider = dnsProvider;
value.validating = true;
value.recordReq = req;
value.recordRes = recordRes;
await this.updateStatus(bean.id, 'validating', "");
await this.updateStatus(bean.id, "validating", "");
value.intervalId = setInterval(async () => {
try {
await checkRecordValue();
} catch (e) {
logger.error('检查cname出错:', e);
logger.error("检查cname出错:", e);
await this.updateError(bean.id, e.message);
}
}, 10000);
}
async updateStatus(id: number, status: CnameRecordStatusType, error?: string) {
const updated: any = {status}
const updated: any = { status };
if (error != null) {
updated.error = error
updated.error = error;
}
await this.getRepository().update(id, updated);
}
async updateError(id: number, error: string) {
await this.getRepository().update(id, {error});
await this.getRepository().update(id, { error });
}
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string,targetCnameDomain:string) {
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
let dnsResolver = null
try{
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain)
}catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败,${e.message}`)
return
let dnsResolver = null;
try {
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
} catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败,${e.message}`);
return;
}
let cnameRecords = []
try{
let cnameRecords = [];
try {
cnameRecords = await dnsResolver.resolveCname(acmeRecordDomain);
}catch (e) {
logger.error(`查询CNAME记录失败:${e.message}`)
return
} catch (e) {
logger.error(`查询CNAME记录失败:${e.message}`);
return;
}
targetCnameDomain = targetCnameDomain.toLowerCase()
targetCnameDomain = punycode.toASCII(targetCnameDomain)
targetCnameDomain = targetCnameDomain.toLowerCase();
targetCnameDomain = punycode.toASCII(targetCnameDomain);
if (cnameRecords.length > 0) {
for (const cnameRecord of cnameRecords) {
if(cnameRecord.toLowerCase() !== targetCnameDomain){
if (cnameRecord.toLowerCase() !== targetCnameDomain) {
//确保只有一个cname记录
throw new Error(`${acmeRecordDomain}存在多个CNAME记录,请删除多余的CNAME记录:${cnameRecord}`)
throw new Error(`${acmeRecordDomain}存在多个CNAME记录,请删除多余的CNAME记录:${cnameRecord}`);
}
}
}
// 确保权威服务器里面没有纯粹的TXT记录
let txtRecords = []
try{
let txtRecords = [];
try {
const txtRecordRes = await dnsResolver.resolveTxt(acmeRecordDomain);
if (txtRecordRes && txtRecordRes.length > 0) {
@@ -414,13 +450,13 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`TXT records: ${JSON.stringify(txtRecords)}`);
txtRecords = txtRecords.concat(...txtRecordRes);
}
}catch (e) {
logger.error(`查询Txt记录失败:${e.message}`)
} catch (e) {
logger.error(`查询Txt记录失败:${e.message}`);
}
if (txtRecords.length === 0) {
//如果权威服务器中查不到txt,无需继续检查
return
return;
}
if (cnameRecords.length > 0) {
// 从cname记录中获取txt记录
@@ -429,11 +465,18 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (res.length > 0) {
for (const txtRecord of txtRecords) {
if (!res.includes(txtRecord)) {
throw new Error(`${acmeRecordDomain}存在多个TXT记录,请删除多余的TXT记录:${txtRecord}`)
throw new Error(`${acmeRecordDomain}存在多个TXT记录,请删除多余的TXT记录:${txtRecord}`);
}
}
}
}
}
async resetStatus(id: number) {
if (!id) {
throw new ValidateException("id不能为空");
}
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
}
@@ -11,12 +11,12 @@ import {
import { RoleService } from "../../sys/authority/service/role-service.js";
import { UserEntity } from "../../sys/authority/entity/user.js";
import { cache, utils } from "@certd/basic";
import { LoginErrorException } from "@certd/lib-server/dist/basic/exception/login-error-exception.js";
import { LoginErrorException } from "@certd/lib-server";
import { CodeService } from "../../basic/service/code-service.js";
import { TwoFactorService } from "../../mine/service/two-factor-service.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { isPlus } from "@certd/plus-core";
import { AddonService } from "@certd/lib-server/dist/user/addon/service/addon-service.js";
import { AddonService } from "@certd/lib-server";
/**
* 系统用户
@@ -0,0 +1,65 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { http, logger, utils } from "@certd/basic";
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
import { AddonService, newAddon, PermissionException, ValidateException } from "@certd/lib-server";
/**
* Addon
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AddonGetterService {
@Inject()
taskServiceBuilder: TaskServiceBuilder;
@Inject()
addonService: AddonService;
async getAddonById(id: any, checkUserId: boolean, userId?: number, defaultAddon?:{type:string,name:string} ): Promise<any> {
const serviceGetter = this.taskServiceBuilder.create({
userId
});
const ctx = {
http,
logger,
utils,
serviceGetter
}
if (!id) {
if (!defaultAddon) {
return null;
}
return await newAddon(defaultAddon.type, defaultAddon.name, {}, ctx);
}
const entity = await this.addonService.info(id);
if (entity == null) {
if (!defaultAddon) {
return null;
}
return await newAddon(defaultAddon.type, defaultAddon.name, {}, ctx);
}
if (checkUserId) {
if (userId == null) {
throw new ValidateException("userId不能为空");
}
if (userId !== entity.userId) {
throw new PermissionException("您对该Addon无访问权限");
}
}
const setting = JSON.parse(entity.setting ?? "{}");
const input = {
id: entity.id,
...setting
};
return await newAddon(entity.addonType, entity.type, input, ctx);
}
async getById(id: any, userId: number): Promise<any> {
return await this.getAddonById(id, true, userId);
}
}
@@ -10,6 +10,9 @@ import { DomainVerifierGetter } from "./domain-verifier-getter.js";
import { DomainService } from "../../../cert/service/domain-service.js";
import { SubDomainService } from "../sub-domain-service.js";
const serviceNames = [
'ocrService',
]
export class TaskServiceGetter implements IServiceGetter{
private userId: number;
private appCtx : IMidwayContainer;
@@ -30,7 +33,13 @@ export class TaskServiceGetter implements IServiceGetter{
} else if (serviceName === 'domainVerifierGetter') {
return await this.getDomainVerifierGetter() as T
}else{
throw new Error(`service ${serviceName} not found`)
if(!serviceNames.includes(serviceName)){
throw new Error(`${serviceName} not in whitelist`)
}
const service = await this.appCtx.getAsync(serviceName)
if (! service){
throw new Error(`${serviceName} not found`)
}
}
}
@@ -44,6 +53,7 @@ export class TaskServiceGetter implements IServiceGetter{
return new AccessGetter(this.userId, accessService.getById.bind(accessService));
}
async getCnameProxyService(): Promise<CnameProxyService> {
const cnameRecordService:CnameRecordService = await this.appCtx.getAsync("cnameRecordService")
return new CnameProxyService(this.userId, cnameRecordService.getWithAccessByDomain.bind(cnameRecordService));
@@ -59,10 +69,6 @@ export class TaskServiceGetter implements IServiceGetter{
return new DomainVerifierGetter(this.userId, domainService);
}
}
export type TaskServiceCreateReq = {
userId: number;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
@@ -75,6 +81,10 @@ export class TaskServiceBuilder {
}
}
export type TaskServiceCreateReq = {
userId: number;
}
@@ -0,0 +1,231 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { http, logger, utils } from '@certd/basic';
// 使用@certd/basic包中已有的utils.sp.spawn函数替代自定义的asyncExec
// 该函数已经内置了Windows系统编码问题的解决方案
export type NetTestResult = {
success: boolean; //是否成功
message: string; //结果
testLog: string; //测试日志
error?: string; //执行错误信息
}
@Provide('nettestService')
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class NetTestService {
/**
* 执行Telnet测试
* @param domain 域名
* @param port 端口
* @returns 测试结果
*/
async telnet(domain: string, port: number): Promise<NetTestResult> {
try {
let command = '';
if (this.isWindows()) {
// Windows系统使用PowerShell执行测试,避免输入重定向问题
// 使用PowerShell的Test-NetConnection命令进行端口测试
command = `powershell -Command "& { $result = Test-NetConnection -ComputerName ${domain} -Port ${port} -InformationLevel Quiet; if ($result) { Write-Host '端口连接成功' } else { Write-Host '端口连接失败' } }"`;
} else {
// Linux系统使用nc命令进行端口测试
command = `nc -zv -w 5 ${domain} ${port} 2>&1`;
}
// 使用utils.sp.spawn执行命令,它会自动处理Windows编码问题
const output = await utils.sp.spawn({
cmd: command,
logger: undefined // 可以根据需要传入logger
});
// 判断测试是否成功
const success = this.isWindows()
? output.includes('端口连接成功')
: output.includes('Connected to');
// 处理结果
return {
success,
message: success ? '端口连接测试成功' : '端口连接测试失败',
testLog: output,
};
} catch (error) {
return {
success: false,
message: 'Telnet测试执行失败',
testLog: error.stdout || error.stderr || error?.message || String(error),
error: error.stderr || error?.message || String(error),
};
}
}
/**
* 执行Ping测试
* @param domain 域名
* @returns 测试结果
*/
async ping(domain: string): Promise<NetTestResult> {
try {
let command = '';
if (this.isWindows()) {
// Windows系统ping命令,发送4个包
command = `ping -n 4 ${domain}`;
} else {
// Linux系统ping命令,发送4个包
command = `ping -c 4 ${domain}`;
}
// 使用utils.sp.spawn执行命令
const output = await utils.sp.spawn({
cmd: command,
logger: undefined
});
// 判断测试是否成功
const success = this.isWindows()
? output.includes('TTL=')
: output.includes('time=');
return {
success,
message: success ? 'Ping测试成功' : 'Ping测试失败',
testLog: output,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: 'Ping测试执行失败',
testLog: error.stderr|| error.stdout || errorMessage,
error: errorMessage
};
}
}
private isWindows() {
return process.platform === 'win32';
}
/**
* 执行域名解析测试
* @param domain 域名
* @returns 解析结果
*/
async domainResolve(domain: string): Promise<NetTestResult> {
try {
let command = '';
if (this.isWindows()) {
// Windows系统使用nslookup命令
command = `nslookup ${domain}`;
} else {
// Linux系统优先使用dig命令,如果没有则回退到nslookup
command = `which dig > /dev/null && dig ${domain} || nslookup ${domain}`;
}
// 使用utils.sp.spawn执行命令
const output = await utils.sp.spawn({
cmd: command,
logger: undefined
});
// 判断测试是否成功
const success = output.includes('Address:') || output.includes('IN A') || output.includes('IN AAAA') ||
(this.isWindows() && output.includes('Name:'));
return {
success,
message: success ? '域名解析测试成功' : '域名解析测试失败',
testLog: output,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: '域名解析测试执行失败',
testLog: error.stdoout || error.stderr || errorMessage,
error: errorMessage
};
}
}
async getLocalIP(): Promise<string[]> {
try {
const output = await utils.sp.spawn({
cmd: 'ip a | grep \'inet \' | grep -v \'127.0.0.1\' | awk \'{print $2}\' | cut -d/ -f1',
logger: undefined
});
// 去除 inet 前缀
let ips = output.trim().replace(/inet /g, '');
return ips.split('\n').filter(ip => ip.length > 0);
} catch (error) {
return [error instanceof Error ? error.message : String(error)];
}
}
async getPublicIP(): Promise<string[]> {
try {
const res = await http.request({
url:"https://ipinfo.io/ip",
method:"GET",
})
return[res]
} catch (error) {
return [error instanceof Error ? error.message : String(error)]
}
}
async getDNSservers(): Promise<string[]> {
let dnsServers: string[] = [];
try {
const output = await utils.sp.spawn({
cmd: 'cat /etc/resolv.conf | grep nameserver | awk \'{print $2}\'',
logger: undefined
});
dnsServers = output.trim().split('\n');
} catch (error) {
dnsServers = [error instanceof Error ? error.message : String(error)];
}
try{
/**
* /app # cat /etc/resolv.conf | grep "ExtServers"
# ExtServers: [223.5.5.5 223.6.6.6]
*/
const extDnsServers = await utils.sp.spawn({
cmd: 'cat /etc/resolv.conf | grep "ExtServers"',
logger: undefined
});
const line = extDnsServers.trim()
if (line.includes('ExtServers') && line.includes('[')) {
const extDns = line.substring(line.indexOf('[') + 1, line.indexOf(']')).split(' ');
const dnsList = extDns.map(item=>`Ext:${item}`)
dnsServers = dnsServers.concat(dnsList);
}
} catch (error) {
logger.error('获取DNS ExtServers 服务器失败', error);
// dnsServers.push(error instanceof Error ? error.message : String(error));
}
return dnsServers;
}
/**
* 获取服务器信息(包括本地IP、外网IP和DNS服务器)
* @returns 服务器信息
*/
async serverInfo(): Promise<any> {
const res = {
localIP: [],
publicIP: [],
dnsServers: [],
}
res.localIP = await this.getLocalIP();
res.publicIP = await this.getPublicIP();
res.dnsServers = await this.getDNSservers();
return res
}
}
@@ -36,3 +36,4 @@ export * from './plugin-apisix/index.js'
export * from './plugin-dokploy/index.js'
export * from './plugin-godaddy/index.js'
export * from './plugin-captcha/index.js'
export * from './plugin-xinnet/index.js'
@@ -0,0 +1,105 @@
import { IAccessService } from '@certd/pipeline';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { AliesaAccess, AliyunAccess, AliyunClientV2 } from '@certd/plugin-lib';
@IsDnsProvider({
name: 'aliesa',
title: '阿里ESA',
desc: '阿里ESA DNS解析',
accessType: 'aliesa',
icon: 'svg:icon-aliyun',
order: 0,
})
export class AliesaDnsProvider extends AbstractDnsProvider {
client: AliyunClientV2
async onInstance() {
const access: AliesaAccess = this.ctx.access as AliesaAccess
const accessGetter = await this.ctx.serviceGetter.get("accessService") as IAccessService
const aliAccess = await accessGetter.getById(access.accessId) as AliyunAccess
const endpoint = `esa.${access.region}.aliyuncs.com`
this.client = aliAccess.getClient(endpoint)
}
async getSiteItem(domain: string) {
const ret = await this.client.doRequest({
// 接口名称
action: "ListSites",
// 接口版本
version: "2024-09-10",
// 接口协议
protocol: "HTTPS",
// 接口 HTTP 方法
method: "GET",
authType: "AK",
style: "RPC",
data: {
query: {
SiteName: domain,
// ["SiteSearchType"] = "exact";
SiteSearchType: "exact",
AccessType: "NS"
}
}
})
const list = ret.Sites
if (list?.length === 0) {
throw new Error(`阿里云ESA中不存在此域名站点:${domain},请确认域名已添加到ESA中,且为NS接入方式`);
}
return list[0]
}
async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, domain);
const siteItem = await this.getSiteItem(domain)
const siteId = siteItem.SiteId
const res = await this.client.doRequest({
action: "CreateRecord",
version: "2024-09-10",
method: "POST",
data: {
query: {
SiteId: siteId,
RecordName: fullRecord,
Type: type,
// queries["Ttl"] = 1231311;
Ttl: 100,
Data: JSON.stringify({ Value: value }),
}
}
})
this.logger.info('添加域名解析成功:', fullRecord, value, res.RecordId);
return {
RecordId: res.RecordId,
SiteId: siteId,
}
}
async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
const record = options.recordRes;
await this.client.doRequest({
action: "DeleteRecord",
version: "2024-09-10",
data: {
query: {
RecordId: record.RecordId,
}
}
})
this.logger.info('删除域名解析成功:', record.RecordId);
}
}
new AliesaDnsProvider();
@@ -1 +1,2 @@
import './aliyun-dns-provider.js';
import './aliesa-dns-provider.js';
@@ -6,7 +6,7 @@ import {
createRemoteSelectInputDefine
} from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
import {optionsUtils} from "@certd/basic";
@IsTaskPlugin({
name: 'DeployCertToAliyunApig',
@@ -1,7 +1,7 @@
import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from '@certd/pipeline';
import {AliyunAccess, createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
import {CertApplyPluginNames, CertInfo} from '@certd/plugin-cert';
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
import {optionsUtils} from "@certd/basic";
@IsTaskPlugin({
name: 'DeployCertToAliyunApiGateway',
@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
import { optionsUtils } from '@certd/basic';
import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
@IsTaskPlugin({
name: 'DeployCertToAliyunCDN',
@@ -8,7 +8,7 @@ import {
} from "@certd/plugin-lib";
import { CertInfo } from '@certd/plugin-cert';
import { CertApplyPluginNames} from '@certd/plugin-cert';
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { optionsUtils } from "@certd/basic";
@IsTaskPlugin({
name: 'DeployCertToAliyunDCDN',
title: '阿里云-部署证书至DCDN',
@@ -7,7 +7,7 @@ import {
} from '@certd/plugin-lib';
import {CertInfo, CertReader} from '@certd/plugin-cert';
import { CertApplyPluginNames} from '@certd/plugin-cert';
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
import {optionsUtils} from "@certd/basic";
import {isArray} from "lodash-es";
@IsTaskPlugin({
name: 'DeployCertToAliyunOSS',
@@ -3,7 +3,7 @@ import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { AwsAccess, AwsRegions } from "../access.js";
import { AwsAcmClient } from "../libs/aws-acm-client.js";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { optionsUtils } from "@certd/basic";
@IsTaskPlugin({
name: 'AwsDeployToCloudFront',
@@ -1,2 +1,3 @@
export * from './geetest/index.js';
export * from './image/index.js';
export * from './tencent/index.js';
@@ -0,0 +1,104 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { ICaptchaAddon } from "../api.js";
import { TencentAccess } from "@certd/plugin-lib";
@IsAddon({
addonType:"captcha",
name: 'tencent',
title: '腾讯云验证码',
desc: '',
showTest:false,
})
export class TencentCaptcha extends BaseAddon implements ICaptchaAddon{
@AddonInput({
title: '腾讯云授权',
helper: '腾讯云授权',
component: {
name: 'access-selector',
vModel:"modelValue",
from: "sys",
type: 'tencent', //固定授权类型
},
required: true,
})
accessId :number;
@AddonInput({
title: '验证ID',
component: {
name:"a-input-number",
placeholder: 'CaptchaAppId',
},
helper:"[腾讯云验证码](https://cloud.tencent.com/act/cps/redirect?redirect=37716&cps_key=b3ef73330335d7a6efa4a4bbeeb6b2c9)",
required: true,
})
captchaAppId:number;
@AddonInput({
title: '验证Key',
component: {
placeholder: 'AppSecretKey',
},
required: true,
})
appSecretKey = '';
async onValidate(data?:any) {
if (!data) {
return false
}
const access = await this.getAccess<TencentAccess>(this.accessId)
const sdk =await import("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
const CaptchaClient = sdk.v20190722.Client;
const clientConfig = {
credential: {
secretId: access.secretId,
secretKey: access.secretKey,
},
region: "",
profile: {
httpProfile: {
endpoint: "captcha.tencentcloudapi.com",
},
},
};
// 实例化要请求产品的client对象,clientProfile是可选的
const client = new CaptchaClient(clientConfig);
const params = {
"CaptchaType": 9, //固定值9
"UserIp": "127.0.0.1",
"Ticket": data.ticket,
"Randstr": data.randstr,
"AppSecretKey": this.appSecretKey,
"CaptchaAppId": this.captchaAppId,
};
const res = await client.DescribeCaptchaResult(params)
if (res.CaptchaCode == 1) {
// 验证成功
// verification successful
return true;
} else {
// 验证失败
// verification failed
this.logger.error("腾讯云验证码验证失败",res.CaptchaMsg)
return false;
}
}
async getCaptcha(): Promise<any> {
return {
captchaAppId: this.captchaAppId,
}
}
}
@@ -1,7 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
import { optionsUtils } from '@certd/basic';
import { CertApplyPluginNames} from '@certd/plugin-cert';
@IsTaskPlugin({
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
@@ -6,7 +6,6 @@ import {
SshAccess,
SshClient
} from "@certd/plugin-lib";
import path from "node:path";
@IsTaskPlugin({
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
@@ -57,7 +56,7 @@ export class FnOSDeployToNAS extends AbstractTaskPlugin {
@TaskInput(
createRemoteSelectInputDefine({
title: "证书Id",
helper: "要更新的证书id",
helper: "面板证书请选择fnOS,其他FTP、webdav等证书请选择已使用,可多选(如果证书域名都匹配的话)",
action: FnOSDeployToNAS.prototype.onGetCertList.name
})
)
@@ -87,7 +86,9 @@ export class FnOSDeployToNAS extends AbstractTaskPlugin {
this.logger.info(`----------- 找到证书,开始部署:${item.sum},${item.domain}`)
const certPath = item.certificate;
const keyPath = item.privateKey;
const certDir = path.dirname(keyPath)
const certDir = keyPath.substring(0, keyPath.lastIndexOf("/"));
const fullchainPath = certDir+ "/fullchain.crt"
const caPath = certDir+ "/issuer_certificate.crt"
const cmd = `
sudo tee ${certPath} > /dev/null <<'EOF'
${this.cert.crt}
@@ -95,6 +96,12 @@ EOF
sudo tee ${keyPath} > /dev/null <<'EOF'
${this.cert.key}
EOF
sudo tee ${fullchainPath} > /dev/null <<'EOF'
${this.cert.crt}
EOF
sudo tee ${caPath} > /dev/null <<'EOF'
${this.cert.ic}
EOF
sudo chmod 0755 "${certDir}/" -R
@@ -157,7 +164,7 @@ echo "服务重启完成!"
}
if (!list || list.length === 0) {
throw new Error("没有找到证书,请先在证书管理也没上传一次证书");
throw new Error("没有找到证书,请先在证书管理页面上传一次证书");
}
return list
}
@@ -1,7 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { optionsUtils } from "@certd/basic";
import { JDCloudAccess } from "../access.js";
@IsTaskPlugin({
@@ -1,7 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { optionsUtils } from "@certd/basic";
import { JDCloudAccess } from "../access.js";
@IsTaskPlugin({
@@ -1,7 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine, QiniuAccess, QiniuClient } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
import { optionsUtils } from '@certd/basic';
import { CertApplyPluginNames} from '@certd/plugin-cert';
@IsTaskPlugin({
name: 'QiniuDeployCertToCDN',
@@ -2,7 +2,7 @@ import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { UpyunAccess } from "../access.js";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { optionsUtils } from "@certd/basic";
import { UpyunClient } from "../client.js";
@IsTaskPlugin({
@@ -1,7 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
import { optionsUtils } from "@certd/basic";
import { VolcengineAccess } from "../access.js";
import { VolcengineCdnClient } from "../cdn-client.js";
@@ -0,0 +1,79 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
import { XinnetClient } from "@certd/plugin-plus";
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
*/
@IsAccess({
name: "xinnet",
title: "新网授权",
icon: "lsicon:badge-new-filled",
desc: ""
})
export class XinnetAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: "用户名",
component: {
placeholder: "手机号/用户名"
},
required: true,
encrypt: true
})
username = "";
@AccessInput({
title: "登录密码",
component: {
name: "a-input-password",
vModel: "value",
placeholder: "登录密码"
},
required: true,
encrypt: true
})
password = "";
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
const client = new XinnetClient({
access: this,
logger: this.ctx.logger,
http: this.ctx.http
});
await client.getDomainList({ pageNo: 1, pageSize: 1 });
return "ok";
}
getCacheKey () {
let hashStr = ""
for (const key in this) {
if (Object.prototype.hasOwnProperty.call(this, key)) {
const element = this[key];
hashStr += element;
}
}
const hashCode = this.ctx.utils.hash.sha256(hashStr);
return `xinnet-${hashCode}`;
}
}
new XinnetAccess();
@@ -0,0 +1,110 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { XinnetAccess } from "./access.js";
import { XinnetClient } from "@certd/plugin-plus";
export type XinnetRecord = {
recordId: number;
recordFullName: string;
recordValue: string;
type: string;
serviceCode: string;
dcpCookie: string;
};
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: "xinnet",
title: "新网",
desc: "新网域名解析",
icon: "lsicon:badge-new-filled",
// 这里是对应的 cloudflare的access类型名称
accessType: "xinnet",
order: 7
})
export class XinnetProvider extends AbstractDnsProvider<XinnetRecord> {
access!: XinnetAccess;
async onInstance() {
//一些初始化的操作
// 也可以通过ctx成员变量传递context
this.access = this.ctx.access as XinnetAccess;
}
/**
* 创建dns解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<XinnetRecord> {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, hostRecord, value, type, domain } = options;
this.logger.info("添加域名解析:", fullRecord, value, type, domain);
const client = new XinnetClient({
logger: this.logger,
access: this.access,
http: this.http
});
const res = await client.getDomainList({
searchKey: domain
});
if (!res.list || res.list.length == 0) {
throw new Error("域名不存在");
}
const serviceCode = res.list[0].serviceCode;
const dcpCookie = await client.getDcpCookie({
serviceCode
});
const recordRes = await client.addDomainDnsRecord({
recordName: hostRecord,
type: type,
recordValue: value
}, {
dcpCookie,
serviceCode
});
return {
...recordRes,
serviceCode,
dcpCookie
};
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<XinnetRecord>): Promise<void> {
const client = new XinnetClient({
logger: this.logger,
access: this.access,
http: this.http
});
const recordRes = options.recordRes;
let dcpCookie = recordRes.dcpCookie;
if (!dcpCookie) {
dcpCookie = await client.getDcpCookie({
serviceCode: recordRes.serviceCode
});
}
await client.deleteDomainDnsRecord(recordRes, {
dcpCookie,
serviceCode: recordRes.serviceCode
});
}
}
//实例化这个provider,将其自动注册到系统中
new XinnetProvider();
@@ -0,0 +1,2 @@
export * from './dns-provider.js';
export * from './access.js';