mirror of
https://github.com/certd/certd.git
synced 2026-05-17 05:37:30 +08:00
Merge branch 'v2-dev' into v2-dev-buy
This commit is contained in:
@@ -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 })
|
||||
|
||||
@@ -19,6 +19,8 @@ export class AutoZPrint {
|
||||
|
||||
@Config('https')
|
||||
httpsConfig: HttpsServerOptions;
|
||||
@Config('koa')
|
||||
koaConfig: any;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
@@ -58,6 +60,7 @@ export class AutoZPrint {
|
||||
httpsServer.start({
|
||||
...this.httpsConfig,
|
||||
app: this.app,
|
||||
hostname: this.httpsConfig.hostname || this.koaConfig.hostname,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {logger, safePromise} from '@certd/basic';
|
||||
export type HttpsServerOptions = {
|
||||
enabled: boolean;
|
||||
app?: Application;
|
||||
hostname?: string;
|
||||
port: number;
|
||||
key: string;
|
||||
cert: string;
|
||||
@@ -58,7 +59,7 @@ export class HttpsServer {
|
||||
opts.app.callback()
|
||||
);
|
||||
this.server = httpServer;
|
||||
const hostname = '::';
|
||||
let hostname = opts.hostname || '::';
|
||||
// A function that runs in the context of the http server
|
||||
// and reports what type of server listens on which port
|
||||
function listeningReporter() {
|
||||
@@ -70,7 +71,19 @@ export class HttpsServer {
|
||||
httpServer.listen(opts.port, hostname, listeningReporter);
|
||||
return httpServer;
|
||||
} catch (e) {
|
||||
logger.error('启动https服务失败', e);
|
||||
if ( e.message?.includes("address family not supported")) {
|
||||
hostname = "0.0.0.0"
|
||||
logger.error(`${e.message},尝试监听${hostname}`, e);
|
||||
try{
|
||||
httpServer.listen(opts.port, hostname, listeningReporter);
|
||||
return httpServer;
|
||||
}catch (e) {
|
||||
logger.error('启动https服务失败', e);
|
||||
}
|
||||
}else{
|
||||
logger.error('启动https服务失败', e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
export const GROUP_TYPE_SITE = 'site';
|
||||
|
||||
@Entity('cd_group')
|
||||
export class GroupEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'user_id', comment: '用户id' })
|
||||
userId: number;
|
||||
|
||||
@Column({ name: 'name', comment: '分组名称' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'icon', comment: '图标' })
|
||||
icon: string;
|
||||
|
||||
@Column({ name: 'favorite', comment: '收藏' })
|
||||
favorite: boolean;
|
||||
|
||||
@Column({ name: 'type', comment: '类型', length: 512 })
|
||||
type: string;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
comment: '修改时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseService } from '@certd/lib-server';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { merge } from 'lodash-es';
|
||||
import { GroupEntity } from '../entity/group.js';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class GroupService extends BaseService<GroupEntity> {
|
||||
@InjectEntityModel(GroupEntity)
|
||||
repository: Repository<GroupEntity>;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async add(bean: any) {
|
||||
if (!bean.type) {
|
||||
throw new Error('type is required');
|
||||
}
|
||||
bean = merge(
|
||||
{
|
||||
favorite: false,
|
||||
},
|
||||
bean
|
||||
);
|
||||
return await this.repository.save(bean);
|
||||
}
|
||||
}
|
||||
@@ -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,7 +17,7 @@ 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";
|
||||
@@ -37,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>;
|
||||
@@ -71,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;
|
||||
}
|
||||
@@ -90,63 +90,64 @@ 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();
|
||||
|
||||
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 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('.', '-');
|
||||
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不能为空");
|
||||
}
|
||||
//hostRecord包含所有权校验信息,不允许用户修改hostRecord
|
||||
delete param.hostRecord
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -171,7 +172,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
} else {
|
||||
record.commonDnsProvider = new CommonDnsProvider({
|
||||
config: record.cnameProvider,
|
||||
plusService: this.plusService,
|
||||
plusService: this.plusService
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,19 +181,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服务`);
|
||||
@@ -201,25 +205,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}`;
|
||||
@@ -229,7 +261,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;
|
||||
@@ -251,16 +283,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 serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
|
||||
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
|
||||
const context = {access, logger, http, utils, domainParser,serviceGetter};
|
||||
const context = { access, logger, http, utils, domainParser, serviceGetter };
|
||||
const dnsProvider: IDnsProvider = await createDnsProvider({
|
||||
dnsProviderType: cnameProvider.dnsProviderType,
|
||||
context,
|
||||
context
|
||||
});
|
||||
return dnsProvider;
|
||||
};
|
||||
@@ -268,15 +300,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);
|
||||
}
|
||||
@@ -289,8 +321,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;
|
||||
}
|
||||
|
||||
@@ -301,7 +333,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) {
|
||||
@@ -318,9 +350,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;
|
||||
}
|
||||
};
|
||||
@@ -331,88 +363,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) {
|
||||
@@ -420,13 +452,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记录
|
||||
@@ -435,11 +467,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";
|
||||
|
||||
/**
|
||||
* 系统用户
|
||||
|
||||
@@ -28,6 +28,7 @@ export class UserSiteMonitorSetting extends BaseSettings {
|
||||
cron?:string = undefined;
|
||||
retryTimes?:number = 3;
|
||||
dnsServer?:string[] = undefined;
|
||||
certValidDays?:number = 10;
|
||||
}
|
||||
|
||||
export class UserEmailSetting extends BaseSettings {
|
||||
|
||||
@@ -56,6 +56,12 @@ export class SiteInfoEntity {
|
||||
@Column({ name: 'disabled', comment: '禁用启用' })
|
||||
disabled: boolean;
|
||||
|
||||
@Column({ name: 'remark', comment: '备注', length: 512 })
|
||||
remark: string;
|
||||
|
||||
@Column({ name: 'group_id', comment: '分组id' })
|
||||
groupId: number;
|
||||
|
||||
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createTime: Date;
|
||||
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
|
||||
|
||||
@@ -169,8 +169,9 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
if (!notify) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sendExpiresNotify(site);
|
||||
await this.sendExpiresNotify(site.id);
|
||||
} catch (e) {
|
||||
logger.error("send notify error", e);
|
||||
}
|
||||
@@ -186,7 +187,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.sendCheckErrorNotify(site);
|
||||
await this.sendCheckErrorNotify(site.id);
|
||||
} catch (e) {
|
||||
logger.error("send notify error", e);
|
||||
}
|
||||
@@ -231,8 +232,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
ipErrorCount: errorCount
|
||||
});
|
||||
try {
|
||||
site = await this.info(site.id);
|
||||
await this.sendCheckErrorNotify(site, true);
|
||||
await this.sendCheckErrorNotify(site.id, true);
|
||||
} catch (e) {
|
||||
logger.error("send notify error", e);
|
||||
}
|
||||
@@ -254,7 +254,8 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
return await this.doCheck(site, notify, retryTimes);
|
||||
}
|
||||
|
||||
async sendCheckErrorNotify(site: SiteInfoEntity, fromIpCheck = false) {
|
||||
async sendCheckErrorNotify(siteId: number, fromIpCheck = false) {
|
||||
const site = await this.info(siteId);
|
||||
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
|
||||
// 发邮件
|
||||
@@ -274,14 +275,14 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
);
|
||||
}
|
||||
|
||||
async sendExpiresNotify(site: SiteInfoEntity) {
|
||||
|
||||
const tipDays = 10;
|
||||
async sendExpiresNotify(siteId: number) {
|
||||
const site = await this.info(siteId);
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
|
||||
const tipDays = setting?.certValidDays || 10;
|
||||
|
||||
const expires = site.certExpiresTime;
|
||||
const validDays = dayjs(expires).diff(dayjs(), "day");
|
||||
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
|
||||
const content = `站点名称: ${site.name} \n站点域名: ${site.domain} \n证书域名: ${site.certDomains} \n颁发机构: ${site.certProvider} \n过期时间: ${dayjs(site.certExpiresTime).format("YYYY-MM-DD")} \n`;
|
||||
if (validDays >= 0 && validDays < tipDays) {
|
||||
// 发通知
|
||||
@@ -392,7 +393,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
async doImport(req: { text: string; userId: number }) {
|
||||
async doImport(req: { text: string; userId: number,groupId?:number }) {
|
||||
if (!req.text) {
|
||||
throw new Error("text is required");
|
||||
}
|
||||
@@ -420,17 +421,22 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
} catch (e) {
|
||||
throw new Error(`${item}格式错误`);
|
||||
}
|
||||
|
||||
}
|
||||
if (arr.length > 2) {
|
||||
name = arr[2] || domain;
|
||||
}
|
||||
let remark:string = "";
|
||||
if (arr.length > 3) {
|
||||
remark = arr[3] || "";
|
||||
}
|
||||
|
||||
list.push({
|
||||
domain,
|
||||
name,
|
||||
httpsPort: port,
|
||||
userId: req.userId
|
||||
userId: req.userId,
|
||||
remark,
|
||||
groupId: req.groupId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -43,28 +43,19 @@ export class PipelineEntity {
|
||||
@Column({ name:"is_template", comment: '是否模版', nullable: true, default: false })
|
||||
isTemplate: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'last_history_time',
|
||||
comment: '最后一次执行时间',
|
||||
nullable: true,
|
||||
})
|
||||
@Column({name: 'last_history_time',comment: '最后一次执行时间',nullable: true,})
|
||||
lastHistoryTime: number;
|
||||
|
||||
@Column({name: 'valid_time',comment: '到期时间',nullable: true,default: 0})
|
||||
validTime: number;
|
||||
|
||||
// 变量
|
||||
lastVars: any;
|
||||
|
||||
@Column({
|
||||
name: 'order',
|
||||
comment: '排序',
|
||||
nullable: true,
|
||||
})
|
||||
@Column({name: 'order', comment: '排序', nullable: true,})
|
||||
order: number;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
@Column({name: 'create_time',comment: '创建时间', default: () => 'CURRENT_TIMESTAMP',})
|
||||
createTime: Date;
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class TaskServiceGetter implements IServiceGetter{
|
||||
return await this.getNotificationService() as T
|
||||
} else if (serviceName === 'domainVerifierGetter') {
|
||||
return await this.getDomainVerifierGetter() as T
|
||||
} else{
|
||||
}else{
|
||||
if(!serviceNames.includes(serviceName)){
|
||||
throw new Error(`${serviceName} not in whitelist`)
|
||||
}
|
||||
@@ -53,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));
|
||||
@@ -68,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 {
|
||||
@@ -84,6 +81,10 @@ export class TaskServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
export type TaskServiceCreateReq = {
|
||||
userId: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -255,6 +255,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
bean.title = pipeline.title;
|
||||
}
|
||||
pipeline.id = bean.id;
|
||||
|
||||
if (pipeline.version == null) {
|
||||
pipeline.version = 0;
|
||||
}
|
||||
pipeline.version++;
|
||||
bean.content = JSON.stringify(pipeline);
|
||||
await this.addOrUpdate(bean);
|
||||
await this.registerTrigger(bean);
|
||||
@@ -368,17 +373,21 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
}
|
||||
|
||||
if (immediateTriggerOnce) {
|
||||
await this.trigger(pipeline.id);
|
||||
await sleep(200);
|
||||
try{
|
||||
await this.trigger(pipeline.id);
|
||||
await sleep(200);
|
||||
}catch(e){
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async trigger(id: any, stepId?: string) {
|
||||
async trigger(id: any, stepId?: string , doCheck = false) {
|
||||
const entity: PipelineEntity = await this.info(id);
|
||||
if (isComm()) {
|
||||
await this.checkHasDeployCount(id, entity.userId);
|
||||
if (doCheck) {
|
||||
await this.beforeCheck(entity);
|
||||
}
|
||||
await this.checkUserStatus(entity.userId);
|
||||
this.cron.register({
|
||||
name: `pipeline.${id}.trigger.once`,
|
||||
cron: null,
|
||||
@@ -457,11 +466,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
return;
|
||||
}
|
||||
cron = cron.trim();
|
||||
if (cron.startsWith("* *")) {
|
||||
cron = cron.replace("\* \*", "0 0");
|
||||
if (cron.startsWith("* * ")) {
|
||||
cron = "0 0 " + cron.substring(5);
|
||||
}
|
||||
if (cron.startsWith("*")) {
|
||||
cron = cron.replace("\*", "0");
|
||||
if (cron.startsWith("* ")) {
|
||||
cron = "0 " + cron.substring(2);
|
||||
}
|
||||
const triggerId = trigger.id;
|
||||
const name = this.buildCronKey(pipelineId, triggerId);
|
||||
@@ -485,6 +494,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
logger.info("当前定时器数量:", this.cron.getTaskSize());
|
||||
}
|
||||
|
||||
|
||||
async isPipelineValidTimeEnabled(entity: PipelineEntity) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
if (isPlus() && settings.pipelineValidTimeEnabled){
|
||||
if (entity.validTime > 0 && entity.validTime < Date.now()){
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
@@ -496,20 +516,34 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
await this.doRun(entity, triggerId, stepId);
|
||||
}
|
||||
|
||||
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
|
||||
const id = entity.id;
|
||||
async beforeCheck(entity: PipelineEntity) {
|
||||
const validTimeEnabled = await this.isPipelineValidTimeEnabled(entity)
|
||||
if (!validTimeEnabled) {
|
||||
throw new Error(`流水线${entity.id}已过期,不予执行`);
|
||||
}
|
||||
|
||||
let suite: UserSuiteEntity = null;
|
||||
if (isComm()) {
|
||||
suite = await this.checkHasDeployCount(id, entity.userId);
|
||||
suite = await this.checkHasDeployCount(entity.id, entity.userId);
|
||||
}
|
||||
try {
|
||||
await this.checkUserStatus(entity.userId);
|
||||
await this.checkUserStatus(entity.userId);
|
||||
|
||||
return {
|
||||
suite
|
||||
}
|
||||
}
|
||||
|
||||
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
|
||||
|
||||
let suite:any = null
|
||||
try{
|
||||
const res = await this.beforeCheck(entity);
|
||||
suite = res.suite
|
||||
} catch (e) {
|
||||
logger.info(e.message);
|
||||
return;
|
||||
logger.error(`流水线${entity.id}触发${triggerId}失败:${e.message}`);
|
||||
}
|
||||
|
||||
|
||||
const id = entity.id;
|
||||
const pipeline = JSON.parse(entity.content);
|
||||
if (!pipeline.id) {
|
||||
pipeline.id = id;
|
||||
|
||||
@@ -20,7 +20,7 @@ export class AuthService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async isAdmin(ctx: any) {
|
||||
isAdmin(ctx: any) {
|
||||
const roleIds: number[] = ctx.user.roles;
|
||||
if (roleIds.includes(1)) {
|
||||
return true;
|
||||
|
||||
@@ -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(' open');
|
||||
|
||||
// 处理结果
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user