diff --git a/README.md b/README.md
index 92d8d89b5..6a3b6b8dd 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
* 全自动申请证书(支持所有注册商注册的域名)
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等,目前已支持40+部署插件)
+* 支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式
* 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式
* 邮件通知、webhook通知
* 私有化部署,数据保存本地,授权信息加密存储,镜像由Github Actions构建,过程公开透明
@@ -155,9 +156,6 @@ services:
## 六、一些说明
* 本项目ssl证书提供商为letencrypt/Google/ZeroSSL
* 申请过程遵循acme协议
-* 需要验证域名所有权,一般有两种方式(目前本项目仅支持dns-01)
- * http-01: 在网站根目录下放置一份txt文件
- * dns-01: 需要给域名添加txt解析记录,通配符域名只能用这种方式
* 证书续期:
* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。
diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx
index 93aa68b85..ba7a19b95 100644
--- a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx
+++ b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx
@@ -145,7 +145,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
search: {
show: true
},
- type: "text",
+ type: "copyable",
form: {
rules: [
{ required: true, message: "请输入域名" },
@@ -154,8 +154,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
]
},
column: {
- width: 160,
- sorter: true
+ width: 200,
+ sorter: true,
+ cellRender({ value }) {
+ return (
+
+
+
+ );
+ }
}
},
httpsPort: {
@@ -185,7 +192,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
width: 200,
sorter: true,
- show: true
+ show: true,
+ cellRender({ value }) {
+ return (
+
+ {value}
+
+ );
+ }
}
},
certProvider: {
@@ -199,7 +213,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 200,
- sorter: true
+ sorter: true,
+ cellRender({ value }) {
+ return {value};
+ }
}
},
certStatus: {
@@ -256,7 +273,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false
},
column: {
- sorter: true
+ sorter: true,
+ width: 155
}
},
checkStatus: {
@@ -268,6 +286,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
dict: dict({
data: [
{ label: "正常", value: "ok", color: "green" },
+ { label: "检查中", value: "checking", color: "blue" },
{ label: "异常", value: "error", color: "red" }
]
}),
@@ -291,7 +310,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 200,
- sorter: true
+ sorter: true,
+ cellRender({ value }) {
+ return {value};
+ }
}
},
pipelineId: {
@@ -336,7 +358,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
value: false
},
column: {
- width: 100,
+ width: 90,
sorter: true
}
}
diff --git a/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx b/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx
index 06efebe5f..a2948e00f 100644
--- a/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx
+++ b/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx
@@ -5,7 +5,7 @@ import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue";
-
+import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise => {
return await api.GetList(query);
@@ -286,7 +286,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
component: {
name: "expires-time-text",
vModel: "value",
- mode: "tag"
+ mode: "tag",
+ title: compute(({ value }) => {
+ return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
+ })
}
}
},
diff --git a/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx b/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx
index 623339959..1caf00629 100644
--- a/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx
+++ b/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx
@@ -7,6 +7,7 @@ import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue";
import SuiteDurationSelector from "../setting/suite-duration-selector.vue";
+import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = sysUserSuiteApi;
const pageRequest = async (query: UserPageQuery): Promise => {
@@ -345,7 +346,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
component: {
name: "expires-time-text",
vModel: "value",
- mode: "tag"
+ mode: "tag",
+ title: compute(({ value }) => {
+ return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
+ })
}
}
},
diff --git a/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts b/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts
index 53f5a861a..f1c7e96f4 100644
--- a/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts
+++ b/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts
@@ -40,7 +40,7 @@ export class SiteInfoController extends CrudController {
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const res = await this.service.add(bean);
- await this.service.check(res.id);
+ await this.service.check(res.id, false, 0);
return this.ok(res);
}
@@ -49,7 +49,7 @@ export class SiteInfoController extends CrudController {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
await this.service.update(bean);
- await this.service.check(bean.id);
+ await this.service.check(bean.id, false, 0);
return this.ok();
}
@Post('/info', { summary: Constants.per.authOnly })
@@ -67,14 +67,14 @@ export class SiteInfoController extends CrudController {
@Post('/check', { summary: Constants.per.authOnly })
async check(@Body('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
- await this.service.check(id, false);
+ await this.service.check(id, false, 0);
return this.ok();
}
@Post('/checkAll', { summary: Constants.per.authOnly })
async checkAll() {
const userId = this.getUserId();
- this.service.checkAll(userId);
+ await this.service.checkAllByUsers(userId);
return this.ok();
}
}
diff --git a/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts b/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts
index e3ff67bdc..bc873a10f 100644
--- a/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts
+++ b/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts
@@ -1,6 +1,6 @@
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { PipelineService } from '../pipeline/service/pipeline-service.js';
-import { logger } from '@certd/basic';
+import { logger, utils } from '@certd/basic';
import { SysSettingsService } from '@certd/lib-server';
import { SiteInfoService } from '../monitor/index.js';
import { Cron } from '../cron/cron.js';
@@ -59,13 +59,7 @@ export class AutoCRegisterCron {
break;
}
offset += records.length;
- for (const record of records) {
- try {
- await this.siteInfoService.doCheck(record, true);
- } catch (e) {
- logger.error(`站点${record.name}检查出错:`, e);
- }
- }
+ await this.siteInfoService.checkList(records);
}
logger.info('站点证书检查完成');
diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts
index a180de4f1..bc897249c 100644
--- a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts
+++ b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts
@@ -5,7 +5,7 @@ import { Repository } from 'typeorm';
import { SiteInfoEntity } from '../entity/site-info.js';
import { siteTester } from './site-tester.js';
import dayjs from 'dayjs';
-import { logger } from '@certd/basic';
+import { logger, utils } from '@certd/basic';
import { PeerCertificate } from 'tls';
import { NotificationService } from '../../pipeline/service/notification-service.js';
import { isComm, isPlus } from '@certd/plus-core';
@@ -76,23 +76,35 @@ export class SiteInfoService extends BaseService {
* 检查站点证书过期时间
* @param site
* @param notify
+ * @param retryTimes
*/
- async doCheck(site: SiteInfoEntity, notify = true) {
+ async doCheck(site: SiteInfoEntity, notify = true, retryTimes = 3) {
if (!site?.domain) {
throw new Error('站点域名不能为空');
}
try {
+ await this.update({
+ id: site.id,
+ checkStatus: 'checking',
+ lastCheckTime: dayjs,
+ });
const res = await siteTester.test({
host: site.domain,
port: site.httpsPort,
+ retryTimes,
});
const certi: PeerCertificate = res.certificate;
if (!certi) {
- return;
+ throw new Error('没有发现证书');
}
const expires = certi.valid_to;
- const domains = [certi.subject?.CN, ...certi.subjectaltname?.replaceAll('DNS:', '').split(',')];
+ const allDomains = certi.subjectaltname?.replaceAll('DNS:', '').split(',');
+ const mainDomain = certi.subject?.CN;
+ let domains = allDomains;
+ if (!allDomains.includes(mainDomain)) {
+ domains = [mainDomain, ...allDomains];
+ }
const issuer = `${certi.issuer.O}<${certi.issuer.CN}>`;
const isExpired = dayjs().valueOf() > dayjs(expires).valueOf();
const status = isExpired ? 'expired' : 'ok';
@@ -139,13 +151,14 @@ export class SiteInfoService extends BaseService {
* 检查,但不发邮件
* @param id
* @param notify
+ * @param retryTimes
*/
- async check(id: number, notify = false) {
+ async check(id: number, notify = false, retryTimes = 3) {
const site = await this.info(id);
if (!site) {
throw new Error('站点不存在');
}
- return await this.doCheck(site, notify);
+ return await this.doCheck(site, notify, retryTimes);
}
async sendCheckErrorNotify(site: SiteInfoEntity) {
@@ -206,15 +219,22 @@ export class SiteInfoService extends BaseService {
}
}
- async checkAll(userId: any) {
+ async checkAllByUsers(userId: any) {
if (!userId) {
throw new Error('userId is required');
}
const sites = await this.repository.find({
where: { userId },
});
+ this.checkList(sites);
+ }
+
+ async checkList(sites: SiteInfoEntity[]) {
for (const site of sites) {
- await this.doCheck(site);
+ this.doCheck(site).catch(e => {
+ logger.error(`检查站点证书失败,${site.domain}`, e.message);
+ });
+ await utils.sleep(200);
}
}
}
diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts b/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts
index 93ae58358..464bfb161 100644
--- a/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts
+++ b/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts
@@ -1,4 +1,4 @@
-import { logger } from '@certd/basic';
+import { logger, utils } from '@certd/basic';
import { merge } from 'lodash-es';
import https from 'https';
import { PeerCertificate } from 'tls';
@@ -6,6 +6,7 @@ export type SiteTestReq = {
host: string; // 只用域名部分
port?: number;
method?: string;
+ retryTimes?: number;
};
export type SiteTestRes = {
@@ -14,6 +15,28 @@ export type SiteTestRes = {
export class SiteTester {
async test(req: SiteTestReq): Promise {
logger.info('测试站点:', JSON.stringify(req));
+ const maxRetryTimes = req.retryTimes ?? 3;
+ let tryCount = 0;
+ let result: SiteTestRes = {};
+ while (true) {
+ try {
+ result = await this.doTestOnce(req);
+ return result;
+ } catch (e) {
+ tryCount++;
+ if (tryCount > maxRetryTimes) {
+ logger.error(`测试站点出错,重试${maxRetryTimes}次`, e);
+ throw e;
+ }
+ //指数退避
+ const time = 2 ** tryCount;
+ logger.error(`测试站点出错,${time}s后重试`, e);
+ await utils.sleep(time * 1000);
+ }
+ }
+ }
+
+ async doTestOnce(req: SiteTestReq): Promise {
const agent = new https.Agent({ keepAlive: false });
const options: any = merge(