Compare commits

..

35 Commits

Author SHA1 Message Date
xiaojunnuo
9b420ad33f v1.33.8 2025-04-27 01:56:54 +08:00
xiaojunnuo
5891290672 build: prepare to build 2025-04-27 01:54:19 +08:00
xiaojunnuo
72a7b51d47 fix: 修复http上传方式无法清除记录文件的bug 2025-04-27 01:52:42 +08:00
xiaojunnuo
2943e0e58d chore: oss 库 完善 2025-04-27 01:31:46 +08:00
xiaojunnuo
5abce916a8 chore: 2025-04-25 18:45:42 +08:00
xiaojunnuo
89d4be8a0a chore: 2025-04-25 18:37:29 +08:00
xiaojunnuo
b7113bda23 perf: 七牛oss支持删除过期备份 2025-04-25 18:36:49 +08:00
xiaojunnuo
0088929622 perf: 支持反向代理增加contextPath路径 2025-04-25 18:04:59 +08:00
xiaojunnuo
b3468cf7f2 perf: 支持阿里云中文域名申请 2025-04-25 18:04:24 +08:00
xiaojunnuo
f88c5c8528 chore: 2025-04-25 02:11:23 +08:00
xiaojunnuo
687fdda7f7 chore: 2025-04-25 02:11:08 +08:00
xiaojunnuo
aec51e514c chore: 2025-04-25 01:44:15 +08:00
xiaojunnuo
308d4600ef perf: 数据库备份支持oss 2025-04-25 01:26:04 +08:00
xiaojunnuo
50a5fa15bb fix: 修复token过期后,疯狂打印token过期信息的bug 2025-04-24 23:54:09 +08:00
xiaojunnuo
7d96a57d73 chore: 2025-04-24 17:27:13 +08:00
xiaojunnuo
162ebfd4e0 perf: 支持中文域名 2025-04-24 11:55:14 +08:00
xiaojunnuo
a586a92d5e perf: 从域名的soa获取主域名,子域名托管无需额外配置 2025-04-24 11:54:54 +08:00
xiaojunnuo
3df20a924f fix: 修复复制流水线无效的bug 2025-04-24 09:09:38 +08:00
xiaojunnuo
ddcf466e4e docs: 升级前切记备份数据 2025-04-23 17:59:48 +08:00
xiaojunnuo
5d10cbf18d fix: 服务器时间获取不准确的bug 2025-04-23 14:55:51 +08:00
xiaojunnuo
8d9afa7592 build: publish 2025-04-22 22:32:56 +08:00
xiaojunnuo
95e05336c2 build: trigger build image 2025-04-22 22:32:18 +08:00
xiaojunnuo
a188385817 v1.33.7 2025-04-22 22:27:50 +08:00
xiaojunnuo
0a6baf331b build: prepare to build 2025-04-22 22:12:51 +08:00
xiaojunnuo
0e29e052d5 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-04-22 22:11:32 +08:00
xiaojunnuo
d8d255980e chore: 2025-04-22 15:53:19 +08:00
xiaojunnuo
dc5a5fa543 chore: 2025-04-22 11:41:45 +08:00
xiaojunnuo
8638fc91ff perf: 证书申请支持51dns 2025-04-22 11:39:09 +08:00
xiaojunnuo
96a0900edc perf: 支持51dns 2025-04-22 11:39:07 +08:00
xiaojunnuo
abea80e3ab perf: 添加部署证书至火山 Live
- 新增 VolcengineDeployToLive 插件,用于将证书部署到火山引擎视频直播
- 新增 VolcengineDeployToVOD 插件,用于将证书部署到火山引擎视频点播
- 更新 ve-client.ts,增加对 Live 和 VOD 服务的支持
2025-04-21 23:39:33 +08:00
xiaojunnuo
42dfe936b7 perf: ssh伪终端模式优化,windows下不开启 2025-04-21 17:34:26 +08:00
xiaojunnuo
8385bcc2d7 perf: ssh PTY模式登录设置 2025-04-21 17:26:42 +08:00
xiaojunnuo
9b8f60b64b perf: 优化首页插件列表展示 2025-04-21 12:13:01 +08:00
xiaojunnuo
474114236e build: publish 2025-04-21 00:08:56 +08:00
xiaojunnuo
238b0b421a build: trigger build image 2025-04-21 00:08:38 +08:00
100 changed files with 3504 additions and 689 deletions

View File

@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
### Bug Fixes
* 服务器时间获取不准确的bug ([5d10cbf](https://github.com/certd/certd/commit/5d10cbf18daf94a90a7551641a3b13e3c5fec611))
* 修复复制流水线无效的bug ([3df20a9](https://github.com/certd/certd/commit/3df20a924f32970b052e2588ea20de095f0ea693))
* 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/certd/certd/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73))
* 修复token过期后疯狂打印token过期信息的bug ([50a5fa1](https://github.com/certd/certd/commit/50a5fa15bb240a125bbc91d2ce1ff3c835888a77))
### Performance Improvements
* 从域名的soa获取主域名子域名托管无需额外配置 ([a586a92](https://github.com/certd/certd/commit/a586a92d5e32ea846ac37be52a7ad8c328d89966))
* 七牛oss支持删除过期备份 ([b7113bd](https://github.com/certd/certd/commit/b7113bda2378116d6c116dc583f563cce7cf9f00))
* 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea))
* 支持阿里云中文域名申请 ([b3468cf](https://github.com/certd/certd/commit/b3468cf7f28228d7c9cf68de6b5a9bbeb67f2c6d))
* 支持反向代理增加contextPath路径 ([0088929](https://github.com/certd/certd/commit/0088929622160cc922995de9a563e8061686ff34))
* 支持中文域名 ([162ebfd](https://github.com/certd/certd/commit/162ebfd4e0c25727efb33952d3bbf7420a02e2c3))
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
### Performance Improvements
* 添加部署证书至火山 Live ([abea80e](https://github.com/certd/certd/commit/abea80e3ab9b1672aebe1c5d5e856693b29931a8))
* 优化首页插件列表展示 ([9b8f60b](https://github.com/certd/certd/commit/9b8f60b64b5f9a3db7dfa9b3dcbd9201984358d0))
* 证书申请支持51dns ([8638fc9](https://github.com/certd/certd/commit/8638fc91ff34fccaf12ff9874fd3fa9d2a8c18b7))
* 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d))
* ssh PTY模式登录设置 ([8385bcc](https://github.com/certd/certd/commit/8385bcc2d7f2411a07748bb5c53f9eaf4d38d7cc))
* ssh伪终端模式优化windows下不开启 ([42dfe93](https://github.com/certd/certd/commit/42dfe936b773b7bdd82ca3378363252ffffd7b71))
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
### Bug Fixes

View File

@@ -1 +1 @@
23:37
22:32

View File

@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
### Performance Improvements
* 添加部署证书至火山 Live ([abea80e](https://github.com/certd/certd/commit/abea80e3ab9b1672aebe1c5d5e856693b29931a8))
* 优化首页插件列表展示 ([9b8f60b](https://github.com/certd/certd/commit/9b8f60b64b5f9a3db7dfa9b3dcbd9201984358d0))
* 证书申请支持51dns ([8638fc9](https://github.com/certd/certd/commit/8638fc91ff34fccaf12ff9874fd3fa9d2a8c18b7))
* 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d))
* ssh PTY模式登录设置 ([8385bcc](https://github.com/certd/certd/commit/8385bcc2d7f2411a07748bb5c53f9eaf4d38d7cc))
* ssh伪终端模式优化windows下不开启 ([42dfe93](https://github.com/certd/certd/commit/42dfe936b773b7bdd82ca3378363252ffffd7b71))
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
### Bug Fixes
* 上传商用证书直接粘贴文本报错的问题修复无法上传ec加密证书的bug ([5750bb7](https://github.com/certd/certd/commit/5750bb706779da274d8e7a87e71416cb64d2df79))
* 修复下载证书时提示token已过期的问题 ([0e07ae6](https://github.com/certd/certd/commit/0e07ae6ce84dcb9279d3c44060d621566afa593c))
### Performance Improvements
* 更新license时同时绑定url ([78367af](https://github.com/certd/certd/commit/78367af8307f801e778c76d49f0918c21ffe032f))
* 切换到不同的分组后再打开创建对话框,会自动选择分组 ([893dcd4](https://github.com/certd/certd/commit/893dcd4f2487891199ed3e5a3d47a79a75efc942))
* 新增部署到火山引擎ALB/CLB、上传到证书中心 ([c9a3e3d](https://github.com/certd/certd/commit/c9a3e3d9d26f964c7af7b56667936f1414fbf42a))
* 优化/api缓存为0 ([dc05cd4](https://github.com/certd/certd/commit/dc05cd481f186b13375192be965000e6b4b429a5))
* 优化华为cdn插件引用ccm证书 ([b565b4b](https://github.com/certd/certd/commit/b565b4b3b919b71b98ea2517670bc1ef00e00dc9))
* 优化证书流水线创建,支持选择分组 ([d613aa8](https://github.com/certd/certd/commit/d613aa8f3e85d8dc475ef1b62d49394ce7fd7d24))
## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17)
### Performance Improvements

View File

@@ -55,6 +55,11 @@ https://your_server_ip:7002
## 二、升级
::: warning
如果您是第一次升级certd版本切记切记先备份一下数据
:::
### 如果使用固定版本号
1. 修改`docker-compose.yaml`中的镜像版本号
2. 运行`docker compose up -d` 即可

View File

@@ -44,6 +44,11 @@ kill -9 $(lsof -t -i:7001)
./start.sh
```
::: warning
升级certd版本前切记切记先备份一下数据
:::
## 三、数据备份
> 数据默认保存在 `./packages/ui/certd-server/data` 目录下
> 建议配置一条[数据库备份流水线](../../use/backup/) 自动备份

View File

@@ -8,5 +8,9 @@
3. [1Panel面板方式部署升级](./1panel/#三、升级)
4. [源码方式部署](./source/#二、升级)
::: warning
如果您是第一次升级certd版本切记切记先备份一下数据
:::
## 升级日志
[CHANGELOG](../changelogs/CHANGELOG.md)

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.33.6"
"version": "1.33.8"
}

View File

@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/publishlab/node-acme-client/compare/v1.33.7...v1.33.8) (2025-04-26)
### Bug Fixes
* 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/publishlab/node-acme-client/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73))
### Performance Improvements
* 从域名的soa获取主域名子域名托管无需额外配置 ([a586a92](https://github.com/publishlab/node-acme-client/commit/a586a92d5e32ea846ac37be52a7ad8c328d89966))
* 七牛oss支持删除过期备份 ([b7113bd](https://github.com/publishlab/node-acme-client/commit/b7113bda2378116d6c116dc583f563cce7cf9f00))
## [1.33.7](https://github.com/publishlab/node-acme-client/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/acme-client
## [1.33.6](https://github.com/publishlab/node-acme-client/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/acme-client

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.33.6",
"@certd/basic": "^1.33.8",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
@@ -26,7 +26,8 @@
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.5",
"lodash-es": "^4.17.21",
"node-forge": "^1.3.1"
"node-forge": "^1.3.1",
"punycode": "^2.3.1"
},
"devDependencies": {
"@types/node": "^20.14.10",
@@ -67,5 +68,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -117,12 +117,12 @@ export default async (client, userOpts) => {
log(`[auto] [${d}] Trigger challengeCreateFn()`);
try {
const { recordReq, recordRes, dnsProvider, challenge, keyAuthorization } = await opts.challengeCreateFn(authz, keyAuthorizationGetter);
const { recordReq, recordRes, dnsProvider, challenge, keyAuthorization ,httpUploader} = await opts.challengeCreateFn(authz, keyAuthorizationGetter);
clearTasks.push(async () => {
/* Trigger challengeRemoveFn(), suppress errors */
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
try {
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider,httpUploader);
} catch (e) {
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
}

View File

@@ -46,3 +46,5 @@ export * from './axios.js'
export * from './logger.js'
export * from './verify.js'
export * from './error.js'
export * from './util.js'

View File

@@ -340,5 +340,6 @@ export {
formatResponseError,
getAuthoritativeDnsResolver,
retrieveTlsAlpnCertificate,
resolveDomainBySoaRecord
};

View File

@@ -59,7 +59,7 @@ export interface ClientExternalAccountBindingOptions {
export interface ClientAutoOptions {
csr: CsrBuffer | CsrString;
challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise<string>) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string}>;
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any) => Promise<any>;
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any,httpUploader:any) => Promise<any>;
email?: string;
termsOfServiceAgreed?: boolean;
skipChallengeVerification?: boolean;
@@ -204,4 +204,6 @@ export function setLogger(fn: (message: any, ...args: any[]) => void): void;
export function walkTxtRecord(record: any): Promise<string[]>;
export const CancelError: typeof CancelError;
export const CancelError: typeof CancelError;
export function resolveDomainBySoaRecord(domain: string): Promise<string>;

View File

@@ -1,137 +0,0 @@
"use strict";
/**
* acme-client type definition tests
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var acme = require("acme-client");
(function () { return __awaiter(void 0, void 0, void 0, function () {
var accountKey, client, order, authorizations, authorization, challenge, _a, certKey, certCsr;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, acme.crypto.createPrivateKey()];
case 1:
accountKey = _b.sent();
client = new acme.Client({
accountKey: accountKey,
directoryUrl: acme.directory.letsencrypt.staging
});
/* Account */
return [4 /*yield*/, client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:test@example.com']
})];
case 2:
/* Account */
_b.sent();
return [4 /*yield*/, client.createOrder({
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: '*.example.com' },
]
})];
case 3:
order = _b.sent();
return [4 /*yield*/, client.getOrder(order)];
case 4:
_b.sent();
return [4 /*yield*/, client.getAuthorizations(order)];
case 5:
authorizations = _b.sent();
authorization = authorizations[0];
challenge = authorization.challenges[0];
return [4 /*yield*/, client.getChallengeKeyAuthorization(challenge)];
case 6:
_b.sent();
return [4 /*yield*/, client.verifyChallenge(authorization, challenge)];
case 7:
_b.sent();
return [4 /*yield*/, client.completeChallenge(challenge)];
case 8:
_b.sent();
return [4 /*yield*/, client.waitForValidStatus(challenge)];
case 9:
_b.sent();
return [4 /*yield*/, acme.crypto.createCsr({
commonName: 'example.com',
altNames: ['example.com', '*.example.com']
})];
case 10:
_a = _b.sent(), certKey = _a[0], certCsr = _a[1];
return [4 /*yield*/, client.finalizeOrder(order, certCsr)];
case 11:
_b.sent();
return [4 /*yield*/, client.getCertificate(order)];
case 12:
_b.sent();
return [4 /*yield*/, client.getCertificate(order, 'DST Root CA X3')];
case 13:
_b.sent();
/* Auto */
return [4 /*yield*/, client.auto({
csr: certCsr,
challengeCreateFn: function (authz, challenge, keyAuthorization) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/];
}); }); },
challengeRemoveFn: function (authz, challenge, keyAuthorization) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/];
}); }); }
})];
case 14:
/* Auto */
_b.sent();
return [4 /*yield*/, client.auto({
csr: certCsr,
email: 'test@example.com',
termsOfServiceAgreed: false,
skipChallengeVerification: false,
challengePriority: ['http-01', 'dns-01'],
preferredChain: 'DST Root CA X3',
challengeCreateFn: function (authz, challenge, keyAuthorization) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/];
}); }); },
challengeRemoveFn: function (authz, challenge, keyAuthorization) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/];
}); }); }
})];
case 15:
_b.sent();
return [2 /*return*/];
}
});
}); })();

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/basic
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
### Performance Improvements
* 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d))
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/basic

View File

@@ -1 +1 @@
00:04
01:54

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -44,5 +44,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -1,13 +1,13 @@
import axios, { AxiosHeaders, AxiosRequestConfig } from 'axios';
import { ILogger, logger } from './util.log.js';
import { Logger } from 'log4js';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import nodeHttp from 'http';
import * as https from 'node:https';
import { merge } from 'lodash-es';
import { safePromise } from './util.promise.js';
import fs from 'fs';
import axios, { AxiosHeaders, AxiosRequestConfig } from "axios";
import { ILogger, logger } from "./util.log.js";
import { Logger } from "log4js";
import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
import nodeHttp from "http";
import * as https from "node:https";
import { merge } from "lodash-es";
import { safePromise } from "./util.promise.js";
import fs from "fs";
export class HttpError extends Error {
status?: number;
statusText?: string;
@@ -22,10 +22,10 @@ export class HttpError extends Error {
super(error.message || error.response?.statusText);
const message = error?.message;
if (message && typeof message === 'string') {
if (message.indexOf && message.indexOf('ssl3_get_record:wrong version number') >= 0) {
if (message && typeof message === "string") {
if (message.indexOf && message.indexOf("ssl3_get_record:wrong version number") >= 0) {
this.message = `${message}(http协议错误服务端要求http协议请检查是否使用了https请求)`;
} else if (message.indexOf('getaddrinfo EAI_AGAIN') >= 0) {
} else if (message.indexOf("getaddrinfo EAI_AGAIN") >= 0) {
this.message = `${message}(无法解析域名请检查网络连接或dns配置更换docker-compose.yaml中dns配置)`;
}
}
@@ -47,7 +47,7 @@ export class HttpError extends Error {
};
let url = error.config?.url;
if (error.config?.baseURL) {
url = (error.config?.baseURL || '') + url;
url = (error.config?.baseURL || "") + url;
}
if (url) {
this.message = `${this.message}${url}`;
@@ -73,7 +73,7 @@ export const HttpCommonError = HttpError;
let defaultAgents = createAgent();
export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) {
logger.info('setGlobalProxy:', opts);
logger.info("setGlobalProxy:", opts);
defaultAgents = createAgent(opts);
}
@@ -102,12 +102,12 @@ export function createAxiosService({ logger }: { logger: Logger }) {
if (config.skipSslVerify || config.httpProxy) {
let rejectUnauthorized = true;
if (config.skipSslVerify) {
logger.info('跳过SSL验证');
logger.info("跳过SSL验证");
rejectUnauthorized = false;
}
const proxy: any = {};
if (config.httpProxy) {
logger.info('使用自定义http代理:', config.httpProxy);
logger.info("使用自定义http代理:", config.httpProxy);
proxy.httpProxy = config.httpProxy;
proxy.httpsProxy = config.httpProxy;
}
@@ -128,7 +128,7 @@ export function createAxiosService({ logger }: { logger: Logger }) {
},
(error: Error) => {
// 发送失败
logger.error('接口请求失败:', error);
logger.error("接口请求失败:", error);
return Promise.reject(error);
}
);
@@ -143,7 +143,7 @@ export function createAxiosService({ logger }: { logger: Logger }) {
logger.info(`http response : status=${response?.status},data=${resData}`);
} else {
logger.info('http response status:', response?.status);
logger.info("http response status:", response?.status);
}
if (response?.config?.returnResponse) {
return response;
@@ -154,53 +154,51 @@ export function createAxiosService({ logger }: { logger: Logger }) {
const status = error.response?.status;
switch (status) {
case 400:
error.message = '请求错误';
error.message = "请求错误";
break;
case 401:
error.message = '认证/登录失败';
error.message = "认证/登录失败";
break;
case 403:
error.message = '拒绝访问';
error.message = "拒绝访问";
break;
case 404:
error.message = `请求地址出错`;
break;
case 408:
error.message = '请求超时';
error.message = "请求超时";
break;
case 500:
error.message = '服务器内部错误';
error.message = "服务器内部错误";
break;
case 501:
error.message = '服务未实现';
error.message = "服务未实现";
break;
case 502:
error.message = '网关错误';
error.message = "网关错误";
break;
case 503:
error.message = '服务不可用';
error.message = "服务不可用";
break;
case 504:
error.message = '网关超时';
error.message = "网关超时";
break;
case 505:
error.message = 'HTTP版本不受支持';
error.message = "HTTP版本不受支持";
break;
default:
break;
}
logger.error(
`请求出错status:${error.response?.status},statusText:${error.response?.statusText},url:${error.config?.url},method:${error.config?.method}`
);
logger.error('返回数据:', JSON.stringify(error.response?.data));
logger.error(`请求出错status:${error.response?.status},statusText:${error.response?.statusText},url:${error.config?.url},method:${error.config?.method}`);
logger.error("返回数据:", JSON.stringify(error.response?.data));
if (error.response?.data) {
const message = error.response.data.message || error.response.data.msg || error.response.data.error;
if (typeof message === 'string') {
if (typeof message === "string") {
error.message = message;
}
}
if (error instanceof AggregateError) {
logger.error('AggregateError', error);
logger.error("AggregateError", error);
}
const err = new HttpError(error);
return Promise.reject(err);
@@ -244,24 +242,24 @@ export function createAgent(opts: CreateAgentOptions = {}) {
if (httpProxy) {
process.env.HTTP_PROXY = httpProxy;
process.env.http_proxy = httpProxy;
logger.info('use httpProxy:', httpProxy);
logger.info("use httpProxy:", httpProxy);
httpAgent = new HttpProxyAgent(httpProxy, opts as any);
merge(httpAgent.options, opts);
} else {
process.env.HTTP_PROXY = '';
process.env.http_proxy = '';
process.env.HTTP_PROXY = "";
process.env.http_proxy = "";
httpAgent = new nodeHttp.Agent(opts);
}
const httpsProxy = opts.httpsProxy;
if (httpsProxy) {
process.env.HTTPS_PROXY = httpsProxy;
process.env.https_proxy = httpsProxy;
logger.info('use httpsProxy:', httpsProxy);
logger.info("use httpsProxy:", httpsProxy);
httpsAgent = new HttpsProxyAgent(httpsProxy, opts as any);
merge(httpsAgent.options, opts);
} else {
process.env.HTTPS_PROXY = '';
process.env.https_proxy = '';
process.env.HTTPS_PROXY = "";
process.env.https_proxy = "";
httpsAgent = new https.Agent(opts);
}
return {
@@ -276,27 +274,27 @@ export async function download(req: { http: HttpClient; config: HttpRequestConfi
http
.request({
logRes: false,
responseType: 'stream',
responseType: "stream",
...config,
})
.then(res => {
const writer = fs.createWriteStream(savePath);
res.pipe(writer);
writer.on('close', () => {
logger.info('文件下载成功');
writer.on("close", () => {
logger.info("文件下载成功");
resolve(true);
});
//error
writer.on('error', err => {
logger.error('下载失败', err);
writer.on("error", err => {
logger.error("下载失败", err);
reject(err);
});
//进度条打印
const totalLength = res.headers['content-length'];
const totalLength = res.headers["content-length"];
let currentLength = 0;
// 每5%打印一次
const step = (totalLength / 100) * 5;
res.on('data', (chunk: any) => {
res.on("data", (chunk: any) => {
currentLength += chunk.length;
if (currentLength % step < chunk.length) {
const percent = ((currentLength / totalLength) * 100).toFixed(2);
@@ -305,19 +303,19 @@ export async function download(req: { http: HttpClient; config: HttpRequestConfi
});
})
.catch(err => {
logger.info('下载失败', err);
logger.info("下载失败", err);
reject(err);
});
});
}
export function getCookie(response: any, name: string) {
const cookies = response.headers['set-cookie'];
const cookies = response.headers["set-cookie"];
//根据name 返回对应的cookie
const found = cookies.find((cookie: any) => cookie.includes(name));
if (!found) {
return null;
}
const cookie = found.split(';')[0];
return cookie.substring(cookie.indexOf('=') + 1);
const cookie = found.split(";")[0];
return cookie.substring(cookie.indexOf("=") + 1);
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/pipeline
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/pipeline
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/pipeline

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,8 +16,8 @@
"test": "mocha --loader=ts-node/esm"
},
"dependencies": {
"@certd/basic": "^1.33.6",
"@certd/plus-core": "^1.33.6",
"@certd/basic": "^1.33.8",
"@certd/plus-core": "^1.33.8",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/lib-huawei
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/lib-huawei
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -23,5 +23,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/lib-iframe
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/lib-iframe
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -30,5 +30,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/jdcloud
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/jdcloud
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/jdcloud

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.33.6",
"version": "1.33.8",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -60,5 +60,5 @@
"fetch"
]
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/lib-k8s
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/lib-k8s
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,7 +16,7 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/basic": "^1.33.6",
"@certd/basic": "^1.33.8",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/lib-server
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/lib-server
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/lib-server

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.33.6",
"version": "1.33.8",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -27,10 +27,10 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.33.6",
"@certd/basic": "^1.33.6",
"@certd/pipeline": "^1.33.6",
"@certd/plus-core": "^1.33.6",
"@certd/acme-client": "^1.33.8",
"@certd/basic": "^1.33.8",
"@certd/pipeline": "^1.33.8",
"@certd/plus-core": "^1.33.8",
"@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.20.3",
"@midwayjs/i18n": "~3.20.3",
@@ -61,5 +61,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.33.6",
"version": "1.33.8",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
### Bug Fixes
* 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/certd/certd/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73))
### Performance Improvements
* 从域名的soa获取主域名子域名托管无需额外配置 ([a586a92](https://github.com/certd/certd/commit/a586a92d5e32ea846ac37be52a7ad8c328d89966))
* 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea))
* 支持阿里云中文域名申请 ([b3468cf](https://github.com/certd/certd/commit/b3468cf7f28228d7c9cf68de6b5a9bbeb67f2c6d))
* 支持中文域名 ([162ebfd](https://github.com/certd/certd/commit/162ebfd4e0c25727efb33952d3bbf7420a02e2c3))
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
**Note:** Version bump only for package @certd/plugin-cert
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/plugin-cert

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -15,15 +15,16 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.33.6",
"@certd/basic": "^1.33.6",
"@certd/pipeline": "^1.33.6",
"@certd/plugin-lib": "^1.33.6",
"@certd/acme-client": "^1.33.8",
"@certd/basic": "^1.33.8",
"@certd/pipeline": "^1.33.8",
"@certd/plugin-lib": "^1.33.8",
"@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"psl": "^1.9.0",
"punycode": "^2.3.1",
"rimraf": "^5.0.5"
},
"devDependencies": {
@@ -41,5 +42,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -35,6 +35,8 @@ export interface IDnsProvider<T = any> {
createRecord(options: CreateRecordOptions): Promise<T>;
removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
setCtx(ctx: DnsProviderContext): void;
//中文域名是否需要punycode转码如果返回True则使用punycode来添加解析记录否则使用中文域名添加解析记录
usePunyCode(): boolean;
}
export interface ISubDomainsGetter {

View File

@@ -8,6 +8,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
http!: HttpClient;
logger!: ILogger;
usePunyCode(): boolean {
return false;
}
setCtx(ctx: DnsProviderContext) {
this.ctx = ctx;
this.logger = ctx.logger;

View File

@@ -1,6 +1,8 @@
import { IDomainParser, ISubDomainsGetter } from "./api";
//@ts-ignore
import psl from "psl";
import { resolveDomainBySoaRecord } from "@certd/acme-client";
import { logger, utils } from "@certd/basic";
export class DomainParser implements IDomainParser {
subDomainsGetter: ISubDomainsGetter;
@@ -17,16 +19,38 @@ export class DomainParser implements IDomainParser {
}
async parse(fullDomain: string) {
const subDomains = await this.subDomainsGetter.getSubDomains();
if (subDomains && subDomains.length > 0) {
for (const subDomain of subDomains) {
if (fullDomain.endsWith(subDomain)) {
//找到子域名托管
return subDomain;
}
logger.info(`查找主域名:${fullDomain}`);
const cacheKey = `domain_parse:${fullDomain}`;
const value = utils.cache.get(cacheKey);
if (value) {
logger.info(`从缓存获取到主域名:${fullDomain}->${value}`);
return value;
}
try {
const mainDomain = await resolveDomainBySoaRecord(fullDomain);
if (mainDomain) {
utils.cache.set(cacheKey, mainDomain, {
ttl: 2 * 60 * 1000,
});
logger.info(`获取到主域名:${fullDomain}->${mainDomain}`);
return mainDomain;
}
} catch (e) {
logger.error("从SOA获取主域名失败", e.message);
}
return this.parseDomain(fullDomain);
// const subDomains = await this.subDomainsGetter.getSubDomains();
// if (subDomains && subDomains.length > 0) {
// for (const subDomain of subDomains) {
// if (fullDomain.endsWith(subDomain)) {
// //找到子域名托管
// return subDomain;
// }
// }
// }
const res = this.parseDomain(fullDomain);
logger.info(`从psl获取主域名:${fullDomain}->${res}`);
return res;
}
}

View File

@@ -6,8 +6,8 @@ import { Challenge } from "@certd/acme-client/types/rfc8555";
import { IContext } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
import { IDnsProvider, IDomainParser } from "../../dns-provider/index.js";
import { HttpChallengeUploader } from "./uploads/api.js";
import punycode from "node:punycode";
import { IOssClient } from "@certd/plugin-lib";
export type CnameVerifyPlan = {
type?: string;
domain: string;
@@ -18,7 +18,7 @@ export type CnameVerifyPlan = {
export type HttpVerifyPlan = {
type: string;
domain: string;
httpUploader: HttpChallengeUploader;
httpUploader: IOssClient;
};
export type DomainVerifyPlan = {
@@ -35,7 +35,6 @@ export type DomainsVerifyPlan = {
export type Providers = {
dnsProvider?: IDnsProvider;
domainsVerifyPlan?: DomainsVerifyPlan;
httpUploader?: HttpChallengeUploader;
};
export type CertInfo = {
@@ -184,7 +183,7 @@ export class AcmeService {
return authz.challenges.find((c: any) => c.type === type);
};
const doHttpVerify = async (challenge: any, httpUploader: HttpChallengeUploader) => {
const doHttpVerify = async (challenge: any, httpUploader: IOssClient) => {
const keyAuthorization = await keyAuthorizationGetter(challenge);
this.logger.info("http校验");
const filePath = `.well-known/acme-challenge/${challenge.token}`;
@@ -203,14 +202,16 @@ export class AcmeService {
this.logger.info("dns校验");
const keyAuthorization = await keyAuthorizationGetter(challenge);
const mainDomain = dnsProvider.usePunyCode() ? domain : punycode.toUnicode(domain);
fullRecord = dnsProvider.usePunyCode() ? fullRecord : punycode.toUnicode(fullRecord);
const recordValue = keyAuthorization;
let hostRecord = fullRecord.replace(`${domain}`, "");
let hostRecord = fullRecord.replace(`${mainDomain}`, "");
if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
}
const recordReq = {
domain,
domain: mainDomain,
fullRecord,
hostRecord,
type: "TXT",
@@ -286,7 +287,7 @@ export class AcmeService {
* @returns {Promise}
*/
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider?: IDnsProvider, httpUploader?: HttpChallengeUploader) {
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider?: IDnsProvider, httpUploader?: IOssClient) {
this.logger.info("执行清理");
/* http-01 */
@@ -321,9 +322,16 @@ export class AcmeService {
isTest?: boolean;
privateKeyType?: string;
}): Promise<CertInfo> {
const { email, isTest, domains, csrInfo, dnsProvider, domainsVerifyPlan, httpUploader } = options;
const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest);
let domains = options.domains;
const encodingDomains = [];
for (const domain of domains) {
encodingDomains.push(punycode.toASCII(domain));
}
domains = encodingDomains;
/* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains);
let privateKey = null;
@@ -361,14 +369,13 @@ export class AcmeService {
privateKey
);
if (dnsProvider == null && domainsVerifyPlan == null && httpUploader == null) {
throw new Error("dnsProvider 、 domainsVerifyPlan 、 httpUploader不能都为空");
if (dnsProvider == null && domainsVerifyPlan == null) {
throw new Error("dnsProvider 、 domainsVerifyPlan不能都为空");
}
const providers: Providers = {
dnsProvider,
domainsVerifyPlan,
httpUploader,
};
/* 自动申请证书 */
const crt = await client.auto({
@@ -383,7 +390,7 @@ export class AcmeService {
): Promise<{ recordReq?: any; recordRes?: any; dnsProvider?: any; challenge: Challenge; keyAuthorization: string }> => {
return await this.challengeCreateFn(authz, keyAuthorizationGetter, providers);
},
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider): Promise<any> => {
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider, httpUploader: IOssClient): Promise<any> => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider, httpUploader);
},
signal: this.options.signal,

View File

@@ -27,8 +27,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
"1、支持多个域名打到一个证书上例如 foo.com*.foo.com*.bar.com\n" +
"2、子域名被通配符包含的不要填写例如www.foo.com已经被*.foo.com包含不要填写www.foo.com\n" +
"3、泛域名只能通配*号那一级(*.foo.com的证书不能用于xxx.yyy.foo.com、不能用于foo.com\n" +
"4、输入一个空格之后再输入下一个\n" +
"5、如果你配置了子域托管解析请先[设置托管子域名](#/certd/pipeline/subDomain)",
"4、输入一个空格之后再输入下一个",
})
domains!: string[];

View File

@@ -9,8 +9,8 @@ import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
import { GoogleClient } from "../../libs/google.js";
import { EabAccess } from "../../access";
import { httpChallengeUploaderFactory } from "./uploads/factory.js";
import { DomainParser } from "../../dns-provider/domain-parser.js";
import { ossClientFactory } from "@certd/plugin-lib";
export * from "./base.js";
export type { CertInfo };
export * from "./cert-reader.js";
@@ -115,6 +115,7 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
})
dnsProviderType!: string;
// dns解析授权类型,勿删
dnsProviderAccessType!: string;
@TaskInput({
@@ -446,7 +447,7 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
rootDir = rootDir + "/";
}
this.logger.info("上传方式", httpRecord.httpUploaderType);
const httpUploader = await httpChallengeUploaderFactory.createUploaderByType(httpRecord.httpUploaderType, {
const httpUploader = await ossClientFactory.createOssClientByType(httpRecord.httpUploaderType, {
access,
rootDir: rootDir,
ctx: httpUploaderContext,

View File

@@ -1,35 +0,0 @@
import { IAccessService } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
export type HttpChallengeUploader = {
upload: (fileName: string, fileContent: Buffer) => Promise<void>;
remove: (fileName: string) => Promise<void>;
};
export type HttpChallengeUploadContext = {
accessService: IAccessService;
logger: ILogger;
utils: typeof utils;
};
export abstract class BaseHttpChallengeUploader<A> implements HttpChallengeUploader {
rootDir: string;
access: A = null;
logger: ILogger;
utils: typeof utils;
ctx: HttpChallengeUploadContext;
protected constructor(opts: { rootDir: string; access: A }) {
this.rootDir = opts.rootDir;
this.access = opts.access;
}
async setCtx(ctx: any) {
// set context
this.ctx = ctx;
this.logger = ctx.logger;
this.utils = ctx.utils;
}
abstract remove(fileName: string): Promise<void>;
abstract upload(fileName: string, fileContent: Buffer): Promise<void>;
}

View File

@@ -1,39 +0,0 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { AliossAccess, AliyunAccess } from "@certd/plugin-lib";
import { AliossClient } from "@certd/plugin-lib";
export class AliossHttpChallengeUploader extends BaseHttpChallengeUploader<AliossAccess> {
async upload(filePath: string, fileContent: Buffer) {
const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId);
const client = new AliossClient({
access: aliyunAccess,
bucket: this.access.bucket,
region: this.access.region,
});
const key = this.rootDir + filePath;
this.logger.info(`开始上传文件: ${key}`);
await client.uploadFile(key, fileContent);
this.logger.info(`校验文件上传成功: ${filePath}`);
}
async remove(filePath: string) {
const key = this.rootDir + filePath;
// remove file from alioss
const client = await this.getAliossClient();
await client.removeFile(key);
this.logger.info(`文件删除成功: ${key}`);
}
private async getAliossClient() {
const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId);
const client = new AliossClient({
access: aliyunAccess,
bucket: this.access.bucket,
region: this.access.region,
});
await client.init();
return client;
}
}

View File

@@ -1,41 +0,0 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { FtpAccess, FtpClient } from "@certd/plugin-lib";
import path from "path";
import os from "os";
import fs from "fs";
export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> {
async upload(filePath: string, fileContent: Buffer) {
const client = new FtpClient({
access: this.access,
logger: this.logger,
});
await client.connect(async (client) => {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
try {
// Write file to temp path
const path = this.rootDir + filePath;
await client.upload(path, tmpFilePath);
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
});
}
async remove(filePath: string) {
const client = new FtpClient({
access: this.access,
logger: this.logger,
});
await client.connect(async (client) => {
const path = this.rootDir + filePath;
await client.client.remove(path);
});
}
}

View File

@@ -1,31 +0,0 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { QiniuOssAccess, QiniuClient, QiniuAccess } from "@certd/plugin-lib";
export class QiniuOssHttpChallengeUploader extends BaseHttpChallengeUploader<QiniuOssAccess> {
async upload(filePath: string, fileContent: Buffer) {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
const client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
if (this.rootDir.endsWith("/")) {
this.rootDir = this.rootDir.slice(0, -1);
}
await client.uploadFile(this.access.bucket, this.rootDir + filePath, fileContent);
}
async remove(filePath: string) {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
const client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
if (this.rootDir.endsWith("/")) {
this.rootDir = this.rootDir.slice(0, -1);
}
await client.removeFile(this.access.bucket, this.rootDir + filePath);
}
}

View File

@@ -1,51 +0,0 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { SshAccess, SshClient } from "@certd/plugin-lib";
import path from "path";
import os from "os";
import fs from "fs";
import { SftpAccess } from "@certd/plugin-lib";
export class SftpHttpChallengeUploader extends BaseHttpChallengeUploader<SftpAccess> {
async upload(filePath: string, fileContent: Buffer) {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
// Write file to temp path
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
const access = await this.ctx.accessService.getById<SshAccess>(this.access.sshAccess);
const key = this.rootDir + filePath;
try {
const client = new SshClient(this.logger);
await client.uploadFiles({
connectConf: access,
mkdirs: true,
transports: [
{
localPath: tmpFilePath,
remotePath: key,
},
],
opts: {
mode: this.access?.fileMode ?? undefined,
},
});
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
}
async remove(filePath: string) {
const access = await this.ctx.accessService.getById<SshAccess>(this.access.sshAccess);
const client = new SshClient(this.logger);
const key = this.rootDir + filePath;
await client.removeFiles({
connectConf: access,
files: [key],
});
}
}

View File

@@ -1,28 +0,0 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { TencentAccess, TencentCosAccess, TencentCosClient } from "@certd/plugin-lib";
export class TencentCosHttpChallengeUploader extends BaseHttpChallengeUploader<TencentCosAccess> {
async upload(filePath: string, fileContent: Buffer) {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
const client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
const key = this.rootDir + filePath;
await client.uploadFile(key, fileContent);
}
async remove(filePath: string) {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
const client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
const key = this.rootDir + filePath;
await client.removeFile(key);
}
}

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
### Bug Fixes
* 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/certd/certd/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73))
### Performance Improvements
* 七牛oss支持删除过期备份 ([b7113bd](https://github.com/certd/certd/commit/b7113bda2378116d6c116dc583f563cce7cf9f00))
* 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea))
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
### Performance Improvements
* ssh PTY模式登录设置 ([8385bcc](https://github.com/certd/certd/commit/8385bcc2d7f2411a07748bb5c53f9eaf4d38d7cc))
* ssh伪终端模式优化windows下不开启 ([42dfe93](https://github.com/certd/certd/commit/42dfe936b773b7bdd82ca3378363252ffffd7b71))
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
**Note:** Version bump only for package @certd/plugin-lib

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.33.6",
"version": "1.33.8",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,10 +16,11 @@
},
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/basic": "^1.33.6",
"@certd/pipeline": "^1.33.6",
"@aws-sdk/client-s3": "^3.787.0",
"@certd/basic": "^1.33.8",
"@certd/pipeline": "^1.33.8",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
"cos-nodejs-sdk-v5": "^2.14.6",
"dayjs": "^1.11.7",
@@ -48,5 +49,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "198a97b00c75219ea8efdc6db4676158506a07c1"
"gitHead": "a188385817e8dbd270dbdbf7f86a5b812884cf59"
}

View File

@@ -1,4 +1,4 @@
import { AliyunAccess } from "../access";
import { AliyunAccess } from "../access/index.js";
export class AliossClient {
access: AliyunAccess;
@@ -52,7 +52,7 @@ export class AliossClient {
}
}
async uploadFile(filePath: string, content: Buffer) {
async uploadFile(filePath: string, content: Buffer | string) {
await this.init();
return await this.client.put(filePath, content);
}
@@ -61,4 +61,23 @@ export class AliossClient {
await this.init();
return await this.client.delete(filePath);
}
async downloadFile(key: string, savePath: string) {
await this.init();
return await this.client.get(key, savePath);
}
async listDir(dirKey: string) {
await this.init();
const res = await this.client.listV2({
prefix: dirKey,
// max-keys: 100,
// continuation-token: "token",
// delimiter: "/",
// marker: "marker",
// encoding-type: "url",
});
return res.objects;
}
}

View File

@@ -11,7 +11,7 @@ export class FtpClient {
this.logger = opts.logger;
}
async connect(callback: (client: FtpClient) => Promise<void>) {
async connect(callback: (client: FtpClient) => Promise<any>) {
const ftp = await import("basic-ftp");
const Client = ftp.Client;
const client = new Client();
@@ -21,7 +21,7 @@ export class FtpClient {
this.logger.info("FTP连接成功");
this.client = client;
try {
await callback(this);
return await callback(this);
} finally {
if (client) {
client.close();
@@ -44,4 +44,20 @@ export class FtpClient {
this.logger.info(`开始删除文件${filePath}`);
await this.client.remove(filePath, true);
}
async listDir(dir: string): Promise<any[]> {
if (!dir) {
return [];
}
if (!dir.endsWith("/")) {
dir = dir + "/";
}
this.logger.info(`开始列出目录${dir}`);
return await this.client.list(dir);
}
async download(filePath: string, savePath: string): Promise<void> {
this.logger.info(`开始下载文件${filePath} -> ${savePath}`);
await this.client.downloadTo(savePath, filePath);
}
}

View File

@@ -5,3 +5,5 @@ export * from "./ftp/index.js";
export * from "./tencent/index.js";
export * from "./qiniu/index.js";
export * from "./ctyun/index.js";
export * from "./oss/index.js";
export * from "./s3/index.js";

View File

@@ -0,0 +1,90 @@
import { IAccessService } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
import dayjs from "dayjs";
export type OssClientRemoveByOpts = {
dir?: string;
//删除多少天前的文件
beforeDays?: number;
};
export type OssFileItem = {
//文件全路径
path: string;
size: number;
//毫秒时间戳
lastModified: number;
};
export type IOssClient = {
upload: (fileName: string, fileContent: Buffer) => Promise<void>;
remove: (fileName: string, opts?: { joinRootDir?: boolean }) => Promise<void>;
download: (fileName: string, savePath: string) => Promise<void>;
removeBy: (removeByOpts: OssClientRemoveByOpts) => Promise<void>;
listDir: (dir: string) => Promise<OssFileItem[]>;
};
export type OssClientContext = {
accessService: IAccessService;
logger: ILogger;
utils: typeof utils;
};
export abstract class BaseOssClient<A> implements IOssClient {
rootDir: string = "";
access: A = null;
logger: ILogger;
utils: typeof utils;
ctx: OssClientContext;
protected constructor(opts: { rootDir?: string; access: A }) {
this.rootDir = opts.rootDir || "";
this.access = opts.access;
}
join(...strs: string[]) {
let res = "";
for (const item of strs) {
if (item) {
if (!res) {
res = item;
} else {
res += "/" + item;
}
}
}
res = res.replace(/[\\/]+/g, "/");
return res;
}
async setCtx(ctx: any) {
// set context
this.ctx = ctx;
this.logger = ctx.logger;
this.utils = ctx.utils;
await this.init();
}
async init() {
// do nothing
}
abstract remove(fileName: string, opts?: { joinRootDir?: boolean }): Promise<void>;
abstract upload(fileName: string, fileContent: Buffer): Promise<void>;
abstract download(fileName: string, savePath: string): Promise<void>;
abstract listDir(dir: string): Promise<OssFileItem[]>;
async removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> {
const list = await this.listDir(removeByOpts.dir);
// removeByOpts.beforeDays = 0;
const beforeDate = dayjs().subtract(removeByOpts.beforeDays, "day");
for (const item of list) {
if (item.lastModified && item.lastModified < beforeDate.valueOf()) {
await this.remove(item.path, { joinRootDir: false });
}
}
}
}

View File

@@ -1,30 +1,33 @@
import { HttpChallengeUploadContext } from "./api";
import { OssClientContext } from "./api";
export class HttpChallengeUploaderFactory {
export class OssClientFactory {
async getClassByType(type: string) {
if (type === "alioss") {
const module = await import("./impls/alioss.js");
return module.AliossHttpChallengeUploader;
return module.default;
} else if (type === "ssh") {
const module = await import("./impls/ssh.js");
return module.SshHttpChallengeUploader;
return module.default;
} else if (type === "sftp") {
const module = await import("./impls/sftp.js");
return module.SftpHttpChallengeUploader;
return module.default;
} else if (type === "ftp") {
const module = await import("./impls/ftp.js");
return module.FtpHttpChallengeUploader;
return module.default;
} else if (type === "tencentcos") {
const module = await import("./impls/tencentcos.js");
return module.TencentCosHttpChallengeUploader;
return module.default;
} else if (type === "qiniuoss") {
const module = await import("./impls/qiniuoss.js");
return module.QiniuOssHttpChallengeUploader;
return module.default;
} else if (type === "s3") {
const module = await import("./impls/s3.js");
return module.default;
} else {
throw new Error(`暂不支持此文件上传方式: ${type}`);
}
}
async createUploaderByType(type: string, opts: { rootDir: string; access: any; ctx: HttpChallengeUploadContext }) {
async createOssClientByType(type: string, opts: { rootDir?: string; access: any; ctx: OssClientContext }) {
const cls = await this.getClassByType(type);
if (cls) {
// @ts-ignore
@@ -35,4 +38,4 @@ export class HttpChallengeUploaderFactory {
}
}
export const httpChallengeUploaderFactory = new HttpChallengeUploaderFactory();
export const ossClientFactory = new OssClientFactory();

View File

@@ -0,0 +1,57 @@
import { BaseOssClient, OssFileItem } from "../api.js";
import { AliossAccess, AliossClient, AliyunAccess } from "../../aliyun/index.js";
import dayjs from "dayjs";
export default class AliOssClientImpl extends BaseOssClient<AliossAccess> {
client: AliossClient;
join(...strs: string[]) {
const str = super.join(...strs);
if (str.startsWith("/")) {
return str.substring(1);
}
return str;
}
async init() {
const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId);
const client = new AliossClient({
access: aliyunAccess,
bucket: this.access.bucket,
region: this.access.region,
});
await client.init();
this.client = client;
}
async download(filePath: string, savePath: string): Promise<void> {
const key = this.join(this.rootDir, filePath);
await this.client.downloadFile(key, savePath);
}
async listDir(dir: string): Promise<OssFileItem[]> {
const dirKey = this.join(this.rootDir, dir) + "/";
const list = await this.client.listDir(dirKey);
this.logger.info(`列出目录: ${dirKey},文件数:${list.length}`);
return list.map(item => {
return {
path: item.name,
lastModified: dayjs(item.lastModified).valueOf(),
size: item.size,
};
});
}
async upload(filePath: string, fileContent: Buffer | string) {
const key = this.join(this.rootDir, filePath);
this.logger.info(`开始上传文件: ${key}`);
await this.client.uploadFile(key, fileContent);
this.logger.info(`文件上传成功: ${filePath}`);
}
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
const key = filePath;
// remove file from alioss
await this.client.removeFile(key);
this.logger.info(`文件删除成功: ${key}`);
}
}

View File

@@ -0,0 +1,78 @@
import { BaseOssClient } from "../api.js";
import path from "path";
import os from "os";
import fs from "fs";
import { FtpAccess, FtpClient } from "../../ftp/index.js";
export default class FtpOssClientImpl extends BaseOssClient<FtpAccess> {
join(...strs: string[]) {
const str = super.join(...strs);
if (!str.startsWith("/")) {
return "/" + str;
}
return str;
}
async download(fileName: string, savePath: string) {
const client = this.getFtpClient();
await client.connect(async client => {
const path = this.join(this.rootDir, fileName);
await client.download(path, savePath);
});
}
async listDir(dir: string) {
const client = this.getFtpClient();
return await client.connect(async (client: FtpClient) => {
const path = this.join(this.rootDir, dir);
const res = await client.listDir(path);
return res.map(item => {
return {
path: this.join(path, item.name),
size: item.size,
lastModified: item.modifiedAt.getTime(),
};
});
});
}
async upload(filePath: string, fileContent: Buffer | string) {
const client = this.getFtpClient();
await client.connect(async client => {
let tmpFilePath = fileContent as string;
if (typeof fileContent !== "string") {
tmpFilePath = path.join(os.tmpdir(), "cert", "oss", filePath);
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
}
try {
// Write file to temp path
const path = this.join(this.rootDir, filePath);
await client.upload(tmpFilePath, path);
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
});
}
private getFtpClient() {
return new FtpClient({
access: this.access,
logger: this.logger,
});
}
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
const client = this.getFtpClient();
await client.connect(async client => {
await client.client.remove(filePath);
this.logger.info(`删除文件成功: ${filePath}`);
});
}
}

View File

@@ -0,0 +1,50 @@
import { QiniuAccess, QiniuClient, QiniuOssAccess } from "../../qiniu/index.js";
import { BaseOssClient, OssFileItem } from "../api.js";
export default class QiniuOssClientImpl extends BaseOssClient<QiniuOssAccess> {
client: QiniuClient;
join(...strs: string[]) {
const str = super.join(...strs);
if (str.startsWith("/")) {
return str.substring(1);
}
return str;
}
async init() {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
this.client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
}
async download(fileName: string, savePath: string): Promise<void> {
const path = this.join(this.rootDir, fileName);
await this.client.downloadFile(this.access.bucket, path, savePath);
}
async listDir(dir: string): Promise<OssFileItem[]> {
const path = this.join(this.rootDir, dir);
const res = await this.client.listDir(this.access.bucket, path);
return res.items.map(item => {
return {
path: item.key,
size: item.fsize,
//ns 纳秒去掉低4位 为毫秒
lastModified: Math.floor(item.putTime / 10000),
};
});
}
async upload(filePath: string, fileContent: Buffer | string) {
const path = this.join(this.rootDir, filePath);
await this.client.uploadFile(this.access.bucket, path, fileContent);
}
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
await this.client.removeFile(this.access.bucket, filePath);
}
}

View File

@@ -0,0 +1,98 @@
import { BaseOssClient, OssFileItem } from "../api.js";
import path from "node:path";
import { S3Access } from "../../s3/access.js";
import fs from "fs";
import dayjs from "dayjs";
export default class S3OssClientImpl extends BaseOssClient<S3Access> {
client: any;
join(...strs: string[]) {
const str = super.join(...strs);
if (str.startsWith("/")) {
return str.substring(1);
}
return str;
}
async init() {
// import { S3Client } from "@aws-sdk/client-s3";
//@ts-ignore
const { S3Client } = await import("@aws-sdk/client-s3");
this.client = new S3Client({
forcePathStyle: true,
//@ts-ignore
s3ForcePathStyle: true,
credentials: {
accessKeyId: this.access.accessKeyId, // 默认 MinIO 访问密钥
secretAccessKey: this.access.secretAccessKey, // 默认 MinIO 秘密密钥
},
region: "us-east-1",
endpoint: this.access.endpoint,
});
}
async download(filePath: string, savePath: string): Promise<void> {
// @ts-ignore
const { GetObjectCommand } = await import("@aws-sdk/client-s3");
const key = path.join(this.rootDir, filePath);
const params = {
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
Key: key, // The name of the object. For example, 'sample_upload.txt'.
};
const res = await this.client.send(new GetObjectCommand({ ...params }));
const fileContent = fs.createWriteStream(savePath);
res.Body.pipe(fileContent);
this.logger.info(`文件下载成功: ${savePath}`);
}
async listDir(dir: string): Promise<OssFileItem[]> {
// @ts-ignore
const { ListObjectsCommand } = await import("@aws-sdk/client-s3");
const dirKey = this.join(this.rootDir, dir);
const params = {
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
Prefix: dirKey, // The name of the object. For example, 'sample_upload.txt'.
};
const res = await this.client.send(new ListObjectsCommand({ ...params }));
return res.Contents.map(item => {
return {
path: item.Key,
size: item.Size,
lastModified: dayjs(item.LastModified).valueOf(),
};
});
}
async upload(filePath: string, fileContent: Buffer | string) {
// @ts-ignore
const { PutObjectCommand } = await import("@aws-sdk/client-s3");
const key = path.join(this.rootDir, filePath);
this.logger.info(`开始上传文件: ${key}`);
const params = {
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
Key: key, // The name of the object. For example, 'sample_upload.txt'.
};
if (typeof fileContent === "string") {
fileContent = fs.createReadStream(fileContent) as any;
}
await this.client.send(new PutObjectCommand({ Body: fileContent, ...params }));
this.logger.info(`文件上传成功: ${filePath}`);
}
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
const key = filePath;
// @ts-ignore
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
await this.client.send(
new DeleteObjectCommand({
Bucket: this.access.bucket,
Key: key,
})
);
this.logger.info(`文件删除成功: ${key}`);
}
}

View File

@@ -0,0 +1,82 @@
import { BaseOssClient, OssFileItem } from "../api.js";
import path from "path";
import os from "os";
import fs from "fs";
import { SftpAccess, SshAccess, SshClient } from "../../ssh/index.js";
export default class SftpOssClientImpl extends BaseOssClient<SftpAccess> {
async download(fileName: string, savePath: string): Promise<void> {
const path = this.join(this.rootDir, fileName);
const client = new SshClient(this.logger);
const access = await this.ctx.accessService.getById<SshAccess>(this.access.sshAccess);
await client.download({
connectConf: access,
filePath: path,
savePath,
});
}
async listDir(dir: string): Promise<OssFileItem[]> {
const path = this.join(this.rootDir, dir);
const client = new SshClient(this.logger);
const access = await this.ctx.accessService.getById<SshAccess>(this.access.sshAccess);
const res = await client.listDir({
connectConf: access,
dir: path,
});
return res.map(item => {
return {
path: this.join(path, item.filename),
size: item.size,
lastModified: item.attrs.atime * 1000,
};
});
}
async upload(filePath: string, fileContent: Buffer | string) {
let tmpFilePath = fileContent as string;
if (typeof fileContent !== "string") {
tmpFilePath = path.join(os.tmpdir(), "cert", "oss", filePath);
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
}
const access = await this.ctx.accessService.getById<SshAccess>(this.access.sshAccess);
const key = this.join(this.rootDir, filePath);
try {
const client = new SshClient(this.logger);
await client.uploadFiles({
connectConf: access,
mkdirs: true,
transports: [
{
localPath: tmpFilePath,
remotePath: key,
},
],
uploadType: "sftp",
opts: {
mode: this.access?.fileMode ?? undefined,
},
});
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
}
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
const access = await this.ctx.accessService.getById<SshAccess>(this.access.sshAccess);
const client = new SshClient(this.logger);
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
await client.removeFiles({
connectConf: access,
files: [filePath],
});
}
}

View File

@@ -1,10 +1,20 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { SshAccess, SshClient } from "@certd/plugin-lib";
import { BaseOssClient, OssClientRemoveByOpts, OssFileItem } from "../api.js";
import path from "path";
import os from "os";
import fs from "fs";
import { SshAccess, SshClient } from "../../ssh/index.js";
export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAccess> {
//废弃
export default class SshOssClientImpl extends BaseOssClient<SshAccess> {
download(fileName: string, savePath: string): Promise<void> {
throw new Error("Method not implemented.");
}
removeBy(removeByOpts: OssClientRemoveByOpts): Promise<void> {
throw new Error("Method not implemented.");
}
listDir(dir: string): Promise<OssFileItem[]> {
throw new Error("Method not implemented.");
}
async upload(filePath: string, fileContent: Buffer) {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
@@ -34,12 +44,14 @@ export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAcces
}
}
async remove(filePath: string) {
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
const client = new SshClient(this.logger);
const key = this.rootDir + filePath;
await client.removeFiles({
connectConf: this.access,
files: [key],
files: [filePath],
});
}
}

View File

@@ -0,0 +1,54 @@
import dayjs from "dayjs";
import { TencentAccess, TencentCosAccess, TencentCosClient } from "../../tencent/index.js";
import { BaseOssClient, OssFileItem } from "../api.js";
export default class TencentOssClientImpl extends BaseOssClient<TencentCosAccess> {
client: TencentCosClient;
join(...strs: string[]) {
const str = super.join(...strs);
if (str.startsWith("/")) {
return str.substring(1);
}
return str;
}
async init() {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
this.client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
}
async download(filePath: string, savePath: string): Promise<void> {
const key = this.join(this.rootDir, filePath);
await this.client.downloadFile(key, savePath);
}
async listDir(dir: string): Promise<OssFileItem[]> {
const dirKey = this.join(this.rootDir, dir) + "/";
// @ts-ignore
const res: any[] = await this.client.listDir(dirKey);
return res.map(item => {
return {
path: item.Key,
size: item.Size,
lastModified: dayjs(item.LastModified).valueOf(),
};
});
}
async upload(filePath: string, fileContent: Buffer | string) {
const key = this.join(this.rootDir, filePath);
await this.client.uploadFile(key, fileContent);
this.logger.info(`文件上传成功: ${filePath}`);
}
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
if (opts?.joinRootDir !== false) {
filePath = this.join(this.rootDir, filePath);
}
await this.client.removeFile(filePath);
this.logger.info(`文件删除成功: ${filePath}`);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./factory.js";
export * from "./api.js";

View File

@@ -1,5 +1,6 @@
import { HttpClient, ILogger } from "@certd/basic";
import { HttpClient, ILogger, utils } from "@certd/basic";
import { QiniuAccess } from "../access.js";
import fs from "fs";
export type QiniuCertInfo = {
key: string;
@@ -98,7 +99,7 @@ export class QiniuClient {
});
}
async uploadFile(bucket: string, key: string, content: Buffer) {
async uploadFile(bucket: string, key: string, content: Buffer | string) {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
@@ -111,8 +112,15 @@ export class QiniuClient {
const config = new qiniu.conf.Config();
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
// 文件上传
const { data, resp } = await formUploader.put(uploadToken, key, content, putExtra);
let res: any = {};
if (typeof content === "string") {
const readableStream = fs.createReadStream(content);
res = await formUploader.putStream(uploadToken, key, readableStream, putExtra);
} else {
// 文件上传
res = await formUploader.put(uploadToken, key, content, putExtra);
}
const { data, resp } = res;
if (resp.statusCode === 200) {
this.logger.info("文件上传成功:" + key);
return data;
@@ -123,12 +131,7 @@ export class QiniuClient {
}
async removeFile(bucket: string, key: string) {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
const config = new qiniu.conf.Config();
config.useHttpsDomain = true;
const bucketManager = new qiniu.rs.BucketManager(mac, config);
const bucketManager = await this.getBucketManager();
const { resp } = await bucketManager.delete(bucket, key);
@@ -139,4 +142,39 @@ export class QiniuClient {
throw new Error("删除失败:" + JSON.stringify(resp));
}
}
async downloadFile(bucket: string, path: string, savePath: string) {
const bucketManager = await this.getBucketManager();
const privateBucketDomain = `http://${bucket}.qiniudn.com`;
const deadline = Math.floor(Date.now() / 1000) + 3600; // 1小时过期
const privateDownloadUrl = bucketManager.privateDownloadUrl(privateBucketDomain, path, deadline);
await utils.request.download({
http: this.http,
logger: this.logger,
config: {
url: privateDownloadUrl,
method: "get",
},
savePath,
});
}
private async getBucketManager() {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
const config = new qiniu.conf.Config();
config.useHttpsDomain = true;
return new qiniu.rs.BucketManager(mac, config);
}
async listDir(bucket: string, path: string) {
const bucketManager = await this.getBucketManager();
const res = await bucketManager.listPrefix(bucket, {
prefix: path,
limit: 1000,
});
return res.data;
}
}

View File

@@ -0,0 +1,87 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: "s3",
title: "s3/minio授权",
desc: "S3/minio oss授权",
icon: "mdi:folder-upload-outline",
})
export class S3Access extends BaseAccess {
@AccessInput({
title: "endpoint",
component: {
placeholder: "http://xxxxxx:9000",
name: "a-input",
vModel: "value",
},
helper: "Minio的地址如果是aws s3 则无需填写",
required: false,
})
endpoint!: string;
/**
* const minioClient = new S3Client({
* endpoint: "http://localhost:9000",
* forcePathStyle: true,
* credentials: {
* accessKeyId: "minioadmin", // 默认 MinIO 访问密钥
* secretAccessKey: "minioadmin", // 默认 MinIO 秘密密钥
* },
* region: "us-east-1",
* });
*/
@AccessInput({
title: "accessKeyId",
component: {
placeholder: "accessKeyId",
},
helper: "accessKeyId",
required: true,
})
accessKeyId!: string;
@AccessInput({
title: "secretAccessKey",
component: {
placeholder: "secretAccessKey",
component: {
name: "a-input",
vModel: "value",
},
},
helper: "secretAccessKey",
encrypt: true,
required: true,
})
secretAccessKey!: string;
@AccessInput({
title: "地区",
value: "us-east-1",
component: {
name: "a-input",
vModel: "value",
},
helper: "region",
required: true,
})
region!: string;
@AccessInput({
title: "存储桶",
component: {
name: "a-input",
vModel: "value",
},
helper: "bucket 名称",
required: true,
})
bucket!: string;
}
new S3Access();

View File

@@ -0,0 +1 @@
export * from "./access.js";

View File

@@ -63,6 +63,16 @@ export class SshAccess extends BaseAccess {
})
passphrase!: string;
@AccessInput({
title: "伪终端",
helper: "如果登录报错all authentication methods failed可以尝试开启伪终端模式进行keyboard-interactive方式登录\n开启后对日志输出有一定的影响",
component: {
name: "a-switch",
vModel: "checked",
},
})
pty!: boolean;
@AccessInput({
title: "socks代理",
helper: "socks代理配置格式socks5://user:password@host:port",

View File

@@ -136,6 +136,20 @@ export class AsyncSsh2Client {
});
}
async listDir(options: { sftp: any; remotePath: string }) {
const { sftp, remotePath } = options;
return new Promise((resolve, reject) => {
this.logger.info(`listDir${remotePath}`);
sftp.readdir(remotePath, (err: Error, list: any) => {
if (err) {
reject(err);
return;
}
resolve(list);
});
});
}
async unlink(options: { sftp: any; remotePath: string }) {
const { sftp, remotePath } = options;
return new Promise((resolve, reject) => {
@@ -170,7 +184,10 @@ export class AsyncSsh2Client {
// }
return new Promise((resolve, reject) => {
this.logger.info(`执行命令:[${this.connConf.host}][exec]: \n` + script);
this.conn.exec(script, { pty: true, env: opts.env }, (err: Error, stream: any) => {
// pty 伪终端window下的输出会带上conhost.exe之类的多余的字符串影响返回结果判断
// linux下 当使用keyboard-interactive 登录时需要pty
const pty = this.connConf.pty; //linux下开启伪终端windows下不开启
this.conn.exec(script, { pty, env: opts.env }, (err: Error, stream: any) => {
if (err) {
reject(err);
return;
@@ -280,6 +297,28 @@ export class AsyncSsh2Client {
}
return proxy;
}
async download(param: { remotePath: string; savePath: string; sftp: any }) {
return new Promise((resolve, reject) => {
const { remotePath, savePath, sftp } = param;
sftp.fastGet(
remotePath,
savePath,
{
step: (transferred: any, chunk: any, total: any) => {
this.logger.info(`${transferred} / ${total}`);
},
},
(err: any) => {
if (err) {
reject(err);
} else {
resolve({});
}
}
);
});
}
}
export class SshClient {
@@ -326,17 +365,17 @@ export class SshClient {
}
}
if (options.uploadType === "sftp") {
const sftp = await conn.getSftp();
for (const transport of transports) {
await conn.fastPut({ sftp, ...transport, opts });
}
} else {
if (options.uploadType === "scp") {
//scp
for (const transport of transports) {
await this.scpUpload({ conn, ...transport, opts });
await new Promise(resolve => setTimeout(resolve, 1000));
}
} else {
const sftp = await conn.getSftp();
for (const transport of transports) {
await conn.fastPut({ sftp, ...transport, opts });
}
}
this.logger.info("文件全部上传成功");
@@ -356,25 +395,29 @@ export class SshClient {
if (err) {
return reject(err);
}
// 准备 SCP 协议头
const fileStats = fs.statSync(localPath);
const fileName = path.basename(localPath);
try {
// 准备 SCP 协议头
const fileStats = fs.statSync(localPath);
const fileName = path.basename(localPath);
// SCP 协议格式C[权限] [文件大小] [文件名]\n
stream.write(`C0644 ${fileStats.size} ${fileName}\n`);
// SCP 协议格式C[权限] [文件大小] [文件名]\n
stream.write(`C0644 ${fileStats.size} ${fileName}\n`);
// 通过管道传输文件
fs.createReadStream(localPath)
.on("error", e => {
this.logger.info("read stream error", e);
reject(e);
})
.pipe(stream)
.on("finish", async () => {
this.logger.info(`上传完成:${localPath} => ${remotePath}`);
resolve(true);
})
.on("error", reject);
// 通过管道传输文件
fs.createReadStream(localPath)
.on("error", e => {
this.logger.info("read stream error", e);
reject(e);
})
.pipe(stream)
.on("finish", async () => {
this.logger.info(`上传完成:${localPath} => ${remotePath}`);
resolve(true);
})
.on("error", reject);
} catch (e) {
reject(e);
}
}
);
} catch (e) {
@@ -475,7 +518,7 @@ export class SshClient {
script = envScripts.join(newLine) + newLine + script;
}
}
return await conn.exec(script as string, { env: options.env });
return await conn.exec(script as string, {});
},
});
}
@@ -523,4 +566,31 @@ export class SshClient {
conn.end();
}
}
async listDir(param: { connectConf: any; dir: string }) {
return await this._call<any>({
connectConf: param.connectConf,
callable: async (conn: AsyncSsh2Client) => {
const sftp = await conn.getSftp();
return await conn.listDir({
sftp,
remotePath: param.dir,
});
},
});
}
async download(param: { connectConf: any; filePath: string; savePath: string }) {
return await this._call<any>({
connectConf: param.connectConf,
callable: async (conn: AsyncSsh2Client) => {
const sftp = await conn.getSftp();
return await conn.download({
sftp,
remotePath: param.filePath,
savePath: param.savePath,
});
},
});
}
}

View File

@@ -1,5 +1,6 @@
import { TencentAccess } from "../access.js";
import { ILogger } from "@certd/basic";
import fs from "fs";
export class TencentCosClient {
access: TencentAccess;
@@ -23,15 +24,19 @@ export class TencentCosClient {
return new sdk.default(clientConfig);
}
async uploadFile(key: string, file: Buffer) {
async uploadFile(key: string, file: Buffer | string) {
const cos = await this.getCosClient();
return new Promise((resolve, reject) => {
let readableStream = file as any;
if (typeof file === "string") {
readableStream = fs.createReadStream(file);
}
cos.putObject(
{
Bucket: this.bucket /* 必须 */,
Region: this.region /* 必须 */,
Key: key /* 必须 */,
Body: file, // 上传文件对象
Body: readableStream, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
},
@@ -66,4 +71,47 @@ export class TencentCosClient {
);
});
}
async downloadFile(key: string, savePath: string) {
const cos = await this.getCosClient();
const writeStream = fs.createWriteStream(savePath);
return new Promise((resolve, reject) => {
cos.getObject(
{
Bucket: this.bucket,
Region: this.region,
Key: key,
Output: writeStream,
},
function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
}
);
});
}
async listDir(dirKey: string) {
const cos = await this.getCosClient();
return new Promise((resolve, reject) => {
cos.getBucket(
{
Bucket: this.bucket,
Region: this.region,
Prefix: dirKey,
MaxKeys: 1000,
},
function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data.Contents);
}
);
});
}
}

View File

@@ -1,12 +1,12 @@
VITE_APP_API=/api
VITE_APP_API=api
#登录与权限关闭
VITE_APP_PM_ENABLED=true
VITE_APP_TITLE=Certd
VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT_YEAR=2021-2024
VITE_APP_COPYRIGHT_YEAR=2021-2025
VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handsfree.work
VITE_APP_LOGO=/static/images/logo/logo.svg
VITE_APP_LOGIN_LOGO=/static/images/logo/rect-black.svg
VITE_APP_LOGO=static/images/logo/logo.svg
VITE_APP_LOGIN_LOGO=static/images/logo/rect-black.svg
VITE_APP_PROJECT_PATH=https://github.com/certd/certd
VITE_APP_NAMESPACE=fs

View File

@@ -1,3 +1,3 @@
VITE_APP_API=/api
VITE_APP_API=api
#登录与权限开启
VITE_APP_PM_ENABLED=true

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
### Bug Fixes
* 服务器时间获取不准确的bug ([5d10cbf](https://github.com/certd/certd/commit/5d10cbf18daf94a90a7551641a3b13e3c5fec611))
* 修复复制流水线无效的bug ([3df20a9](https://github.com/certd/certd/commit/3df20a924f32970b052e2588ea20de095f0ea693))
* 修复token过期后疯狂打印token过期信息的bug ([50a5fa1](https://github.com/certd/certd/commit/50a5fa15bb240a125bbc91d2ce1ff3c835888a77))
### Performance Improvements
* 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea))
* 支持反向代理增加contextPath路径 ([0088929](https://github.com/certd/certd/commit/0088929622160cc922995de9a563e8061686ff34))
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
### Performance Improvements
* 优化首页插件列表展示 ([9b8f60b](https://github.com/certd/certd/commit/9b8f60b64b5f9a3db7dfa9b3dcbd9201984358d0))
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
### Bug Fixes

View File

@@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/api/app/favicon"/>
<link rel="icon" href="api/app/favicon"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Loading</title>
<script src="/static/icons/iconfont.js"></script>
<link rel="stylesheet" type="text/css" href="/static/index.css"/>
<script src="static/icons/iconfont.js?v=<%=version%>"></script>
<link rel="stylesheet" type="text/css" href="static/index.css?v=<%=version%>"/>
</head>
<body>
<div id="app">

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.33.6",
"version": "1.33.8",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -101,8 +101,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.33.6",
"@certd/pipeline": "^1.33.6",
"@certd/lib-iframe": "^1.33.8",
"@certd/pipeline": "^1.33.8",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -120,7 +120,7 @@ function createService() {
errorLog(error, error?.response?.config?.showErrorNotify);
if (status === 401) {
const userStore = useUserStore();
userStore.logout();
userStore.logout(true, true);
}
if (error?.config?.onError) {

View File

@@ -21,6 +21,6 @@ const props = defineProps<{
}>();
const nslookupCmd = computed(() => {
return `nslookup -qa=txt _acme-challenge.${props.record.domain}`;
return `nslookup -q=txt _acme-challenge.${props.record.domain}`;
});
</script>

View File

@@ -75,6 +75,7 @@ const uploaderTypeDict = dict({
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃请选择SFTP方式)", value: "ssh", disabled: true },
],
});

View File

@@ -98,17 +98,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "子域名托管设置",
name: "SubDomain",
path: "/certd/pipeline/subDomain",
component: "/certd/pipeline/sub-domain/index.vue",
meta: {
icon: "material-symbols:approval-delegation-outline",
auth: true,
keepAlive: true,
},
},
// {
// title: "子域名托管设置",
// name: "SubDomain",
// path: "/certd/pipeline/subDomain",
// component: "/certd/pipeline/sub-domain/index.vue",
// meta: {
// icon: "material-symbols:approval-delegation-outline",
// auth: true,
// keepAlive: true,
// },
// },
{
title: "流水线分组管理",
name: "PipelineGroupManager",

View File

@@ -161,10 +161,10 @@ export const useSettingStore = defineStore({
//@ts-ignore
if (this.isComm) {
if (siteInfo.logo) {
siteInfo.logo = `/api/basic/file/download?key=${siteInfo.logo}`;
siteInfo.logo = `api/basic/file/download?key=${siteInfo.logo}`;
}
if (siteInfo.loginLogo) {
siteInfo.loginLogo = `/api/basic/file/download?key=${siteInfo.loginLogo}`;
siteInfo.loginLogo = `api/basic/file/download?key=${siteInfo.loginLogo}`;
}
}
this.siteInfo = _.merge({}, defaultSiteInfo, siteInfo);

View File

@@ -108,10 +108,12 @@ export const useUserStore = defineStore({
/**
* @description: logout
*/
async logout(goLogin = true) {
async logout(goLogin = true, from401 = false) {
this.resetState();
resetAllStores();
await UserApi.logout(); //主要是清空cookie
if (!from401) {
await UserApi.logout(); //主要是清空cookie
}
goLogin && router.push("/login");
mitter.emit("app.logout");
},

View File

@@ -7,7 +7,7 @@
<a-descriptions title="" bordered>
<a-descriptions-item label="用户名">{{ userInfo.username }}</a-descriptions-item>
<a-descriptions-item label="头像">
<a-avatar v-if="userInfo.avatar" size="large" :src="'/api/basic/file/download?&key=' + userInfo.avatar" style="background-color: #eee"> </a-avatar>
<a-avatar v-if="userInfo.avatar" size="large" :src="'api/basic/file/download?&key=' + userInfo.avatar" style="background-color: #eee"> </a-avatar>
<a-avatar v-else size="large" style="background-color: #00b4f5">
{{ userInfo.username }}
</a-avatar>

View File

@@ -120,6 +120,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
function onDialogOpen(opt: any) {
const searchForm = crudExpose.getSearchValidatedFormData();
opt.initialForm = {
...opt.initialForm,
groupId: searchForm.groupId,
};
}

View File

@@ -3,7 +3,7 @@
<div class="header-profile flex-wrap bg-white dark:bg-black">
<div class="flex flex-1">
<div class="avatar">
<a-avatar v-if="userInfo.avatar" size="large" :src="'/api/basic/file/download?&key=' + userInfo.avatar" style="background-color: #eee"> </a-avatar>
<a-avatar v-if="userInfo.avatar" size="large" :src="'api/basic/file/download?&key=' + userInfo.avatar" style="background-color: #eee"> </a-avatar>
<a-avatar v-else size="large" style="background-color: #00b4f5">
{{ userInfo.username }}
</a-avatar>
@@ -103,16 +103,20 @@
<a-row :gutter="10">
<a-col v-for="item of pluginGroups.groups.all.plugins" :key="item.name" class="plugin-item-col" :xl="4" :md="6" :xs="24">
<a-card>
<a-tooltip :title="item.desc" class="flex-between overflow-hidden">
<a-tooltip class="flex-between overflow-hidden">
<template #title>
<div>{{ item.title }}</div>
<div>{{ item.desc }}</div>
</template>
<div class="plugin-item pointer">
<div class="icon">
<fs-icon :icon="item.icon" class="font-size-16 color-blue" />
</div>
<div class="text">
<div class="text flex-1 ellipsis">
<div class="title">{{ item.title }}</div>
</div>
</div>
<div class="flex-o"><vip-button v-if="item.needPlus" mode="icon" class="" /></div>
<div class="flex-o ml-1"><vip-button v-if="item.needPlus" mode="icon" class="" /></div>
</a-tooltip>
</a-card>
</a-col>
@@ -178,7 +182,7 @@ const userInfo: ComputedRef<UserInfoRes> = computed(() => {
return userStore.getUserInfo;
});
const now = computed(() => {
const serverTime = settingStore.app.deltaTime + Date.now();
const serverTime = Date.now() - settingStore.app.deltaTime;
return dayjs(serverTime).format("YYYY-MM-DD HH:mm:ss");
});
@@ -288,12 +292,17 @@ function openUpgradeUrl() {
.plugin-list {
margin: 0 20px;
.ant-card .ant-card-body {
padding: 16px;
}
.plugin-item-col {
margin-bottom: 10px;
.plugin-item {
display: flex;
justify-items: center;
line-height: 20px;
overflow: hidden;
flex: 1;
.icon {
display: flex;
justify-items: center;

View File

@@ -28,7 +28,7 @@ function onChange(value: string) {
const imageCodeUrl = ref();
function resetImageCode() {
const randomStr = nanoid(10);
let url = "/api/basic/code/captcha";
let url = "api/basic/code/captcha";
imageCodeUrl.value = url + "?randomStr=" + randomStr;
emit("update:randomStr", randomStr);
}

View File

@@ -164,7 +164,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
width: "auto",
},
buildUrl(key: string) {
return `/api/basic/file/download?&key=` + key;
return `api/basic/file/download?&key=` + key;
},
},
},
@@ -190,7 +190,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
},
},
buildUrl(key: string) {
return `/api/basic/file/download?&key=` + key;
return `api/basic/file/download?&key=` + key;
},
},
},

View File

@@ -21,9 +21,9 @@ export default ({ command, mode }) => {
const env = loadEnv(mode, process.cwd());
const devServerFs: any = {};
const devAlias: any[] = [];
const base = "/";
const base = "./";
// if (mode.startsWith("dev")) {
// base = "/dev";
// base = "./";
// }
return {
base: base,
@@ -36,6 +36,7 @@ export default ({ command, mode }) => {
data: {
title: env.VITE_APP_TITLE,
projectPath: env.VITE_APP_PROJECT_PATH,
version: env.VITE_APP_VERSION,
},
},
}),

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26)
### Performance Improvements
* 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea))
* 支持反向代理增加contextPath路径 ([0088929](https://github.com/certd/certd/commit/0088929622160cc922995de9a563e8061686ff34))
* 支持中文域名 ([162ebfd](https://github.com/certd/certd/commit/162ebfd4e0c25727efb33952d3bbf7420a02e2c3))
## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22)
### Performance Improvements
* 添加部署证书至火山 Live ([abea80e](https://github.com/certd/certd/commit/abea80e3ab9b1672aebe1c5d5e856693b29931a8))
* 证书申请支持51dns ([8638fc9](https://github.com/certd/certd/commit/8638fc91ff34fccaf12ff9874fd3fa9d2a8c18b7))
* 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d))
## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.33.6",
"version": "1.33.8",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -38,19 +38,19 @@
"@aws-sdk/client-acm": "^3.699.0",
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.33.6",
"@certd/basic": "^1.33.6",
"@certd/commercial-core": "^1.33.6",
"@certd/jdcloud": "^1.33.6",
"@certd/lib-huawei": "^1.33.6",
"@certd/lib-k8s": "^1.33.6",
"@certd/lib-server": "^1.33.6",
"@certd/midway-flyway-js": "^1.33.6",
"@certd/pipeline": "^1.33.6",
"@certd/plugin-cert": "^1.33.6",
"@certd/plugin-lib": "^1.33.6",
"@certd/plugin-plus": "^1.33.6",
"@certd/plus-core": "^1.33.6",
"@certd/acme-client": "^1.33.8",
"@certd/basic": "^1.33.8",
"@certd/commercial-core": "^1.33.8",
"@certd/jdcloud": "^1.33.8",
"@certd/lib-huawei": "^1.33.8",
"@certd/lib-k8s": "^1.33.8",
"@certd/lib-server": "^1.33.8",
"@certd/midway-flyway-js": "^1.33.8",
"@certd/pipeline": "^1.33.8",
"@certd/plugin-cert": "^1.33.8",
"@certd/plugin-lib": "^1.33.8",
"@certd/plugin-plus": "^1.33.8",
"@certd/plus-core": "^1.33.8",
"@corsinvest/cv4pve-api-javascript": "^8.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
@@ -76,6 +76,7 @@
"cos-nodejs-sdk-v5": "^2.14.6",
"cron-parser": "^4.9.0",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.7",
"form-data": "^4.0.0",
"glob": "^11.0.0",

View File

@@ -1,5 +1,5 @@
import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { PlusService } from '@certd/lib-server';
import {CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions} from '@certd/plugin-cert';
import {PlusService} from '@certd/lib-server';
export type CommonCnameProvider = {
id: number;
@@ -24,7 +24,13 @@ export class CommonDnsProvider implements IDnsProvider {
this.plusService = opts.plusService;
}
async onInstance() {}
usePunyCode(): boolean {
return false
}
async onInstance() {
}
async createRecord(options: CreateRecordOptions) {
if (!this.config.domain.endsWith(options.domain)) {
throw new Error('cname服务域名不匹配');
@@ -45,6 +51,7 @@ export class CommonDnsProvider implements IDnsProvider {
});
return res;
}
async removeRecord(options: RemoveRecordOptions<any>) {
const res = await this.plusService.requestWithToken({
url: '/activation/certd/cname/recordRemove',
@@ -60,6 +67,7 @@ export class CommonDnsProvider implements IDnsProvider {
});
return res;
}
setCtx(ctx: DnsProviderContext): void {
this.ctx = ctx;
}

View File

@@ -18,3 +18,4 @@ export * from './plugin-dnsla/index.js';
export * from './plugin-upyun/index.js';
export * from './plugin-volcengine/index.js'
export * from './plugin-jdcloud/index.js'
export * from './plugin-51dns/index.js'

View File

@@ -0,0 +1,40 @@
import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: '51dns',
title: '51dns授权',
icon: 'arcticons:dns-changer-3',
desc: '',
})
export class Dns51Access extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: '用户名',
component: {
placeholder: '用户名或手机号',
},
required: true,
encrypt: false,
})
username = '';
@AccessInput({
title: '登录密码',
component: {
name:"a-input-password",
vModel:"value",
placeholder: '密码',
},
required: true,
encrypt: true,
})
password = '';
}
new Dns51Access();

View File

@@ -0,0 +1,237 @@
import {createAxiosService, HttpClient, ILogger} from "@certd/basic";
import {Dns51Access} from "./access.js";
export class Dns51Client {
logger: ILogger;
access: Dns51Access;
http: HttpClient;
cryptoJs: any;
isLogined = false;
_token = "";
_cookie = "";
constructor(options: {
logger: ILogger;
access: Dns51Access;
}) {
this.logger = options.logger;
this.access = options.access;
this.http = createAxiosService({
logger: this.logger
});
}
aes(val: string) {
if (!this.cryptoJs) {
throw new Error("crypto-js not init");
}
const CryptoJS = this.cryptoJs;
var k = CryptoJS.enc.Utf8.parse("1234567890abcDEF");
var iv = CryptoJS.enc.Utf8.parse("1234567890abcDEF");
return CryptoJS.AES.encrypt(val, k, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
}).toString();
}
async init() {
if (this.cryptoJs) {
return;
}
const CryptoJSModule = await import("crypto-js");
this.cryptoJs = CryptoJSModule.default;
}
async login() {
if (this.isLogined) {
return;
}
await this.init();
const res = await this.http.request({
url: "https://www.51dns.com/login.html",
method: "get",
withCredentials: true,
logRes: false,
returnResponse: true,
headers: {
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36',
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com',
},
});
let setCookie = res.headers['set-cookie']
let cookie = setCookie.map((item: any) => {
return item.split(';')[0]
}).join(';')
//提取 var csrfToken = "ieOfM21eDd9nWJv3OZtMJF6ogDsnPKQHJ17dlMck";
const _token = res.data.match(/var csrfToken = "(.*?)"/)[1];
this.logger.info("_token:", _token);
this._token = _token;
var obj = {
"email_or_phone": this.aes(this.access.username),
"password": this.aes(this.access.password),
"type": this.aes("account"),
"redirectTo": "https://www.51dns.com/domain",
"_token": _token
};
const res2 = await this.http.request({
url: "https://www.51dns.com/login",
method: "post",
data: {
...obj
},
withCredentials: true,
logRes: false,
returnResponse: true,
headers: {
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': cookie,
'X-Requested-With': 'XMLHttpRequest'
}
});
this.logger.info("return headers:", JSON.stringify(res2.headers))
if (res2.data.code == 0) {
setCookie = res2.headers['set-cookie']
this._cookie = setCookie.map((item: any) => {
return item.split(';')[0]
}).join(';')
this.logger.info("cookie:", this._cookie)
this.logger.info("登录成功")
} else {
throw new Error("登录失败:", res2.data)
}
const res3 = await this.http.request({
url: 'https://www.51dns.com/domain',
method: 'get',
withCredentials: true,
logRes: false,
returnResponse: true,
headers: {
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36',
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com/login.html',
'Cookie': this._cookie,
}
})
const success2 = res3.data.includes('<span class="nav-title">DNS解析</span>')
if (!success2) {
throw new Error("检查登录失败")
}
this.logger.info("检查登录成功")
this.isLogined = true;
}
async getDomainId(domain: string) {
await this.login();
const res = await this.http.request({
url: `https://www.51dns.com/domain?domain=${domain}&status=`,
method: "get",
withCredentials: true,
logRes: false,
returnResponse: true,
headers: this.getRequestHeaders()
});
// 提取 <a target="_blank" href="https://www.51dns.com/domain/record/193341603"
// class="color47">certd.top</a>
const regExp = new RegExp(`<a target="_blank" href="https://www.51dns.com/domain/record/(\\d+)"[^>]*>${domain}<\\/a>`, "i");
const matched = res.data.match(regExp);
if (!matched || matched.length < 1) {
throw new Error(`域名${domain}不存在`);
}
const domainId = matched[1];
this.logger.info(`域名${domain}的id为${domainId}`)
return parseInt(domainId);
}
private getRequestHeaders() {
return {
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com',
'Cookie': this._cookie
};
}
async createRecord(param: { domain: string, data: any; domainId: number; host: string; ttl: number; type: string }) {
const {domain, data, host, type} = param;
const domainId = await this.getDomainId(domain);
const url = "https://www.51dns.com/domain/storenNewRecord";
const req = {
_token: this._token,
domain_id: domainId,
record: host,
type: type,
value: data,
ttl: 300,
mx:"",
view_id: 0
};
this.logger.info("req:", JSON.stringify(req))
const res = await this.http.request({
url,
method: "post",
data: req,
withCredentials: true,
headers: {
...this.getRequestHeaders(),
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (res.status !== 200) {
throw new Error(`创建域名解析失败:${res.msg}`);
}
const id = res.data.id;
return {
id,
domainId
};
}
async deleteRecord(param: { domainId: number; id: number }) {
const url = "https://www.51dns.com/domain/operateRecord"
/*
type: delete
ids[0]: 601019779
domain_id: 193341603
_token: ieOfM21eDd9nWJv3OZtMJF6ogDsnPKQHJ17dlMck
*/
const body = {
type: "delete",
ids: [param.id],
domain_id: param.domainId,
_token: this._token
}
const res = await this.http.request({
url,
method: "post",
data: body,
withCredentials: true,
headers: {
...this.getRequestHeaders(),
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (res.status !== 200) {
throw new Error(`删除域名解析失败:${res.msg}`);
}
}
}

View File

@@ -0,0 +1,98 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { Dns51Access } from "./access.js";
import { Dns51Client } from "./client.js";
export type Dns51Record = {
id: number;
domainId: number,
};
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: '51dns',
title: '51dns',
desc: '51DNS',
icon: 'arcticons:dns-changer-3',
// 这里是对应的 cloudflare的access类型名称
accessType: '51dns',
})
export class Dns51DnsProvider extends AbstractDnsProvider<Dns51Record> {
// 通过Autowire传递context
access!: Dns51Access;
client!:Dns51Client;
async onInstance() {
//一些初始化的操作
// 也可以通过ctx成员变量传递context 与Autowire效果一样
this.access = this.ctx.access as Dns51Access;
this.client = new Dns51Client({
logger: this.logger,
access: this.access,
});
}
/**
* 创建dns解析记录用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<Dns51Record> {
/**
* 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 domainId = await this.client.getDomainId(domain);
this.logger.info('获取domainId成功:', domainId);
const res = await this.client.createRecord({
domain: domain,
domainId: domainId,
type: 'TXT',
host: hostRecord,
data: value,
ttl: 300,
})
return {
id: res.id,
domainId: domainId,
};
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<Dns51Record>): Promise<void> {
const { fullRecord, value } = options.recordReq;
const record = options.recordRes;
this.logger.info('删除域名解析:', fullRecord, value);
if (!record) {
this.logger.info('record为空不执行删除');
return;
}
//这里调用删除txt dns解析记录接口
/**
* 请求示例
* DELETE /api/record?id=85371689655342080 HTTP/1.1
* Authorization: Basic {token}
* 请求参数
*/
const {id,domainId} = record
await this.client.deleteRecord({
id,
domainId
})
this.logger.info(`删除域名解析成功:fullRecord=${fullRecord},id=${id}`);
}
}
//实例化这个provider将其自动注册到系统中
new Dns51DnsProvider();

View File

@@ -0,0 +1,3 @@
export * from './dns-provider.js';
export * from './access.js';
export * from './client.js';

View File

@@ -87,15 +87,13 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
// }
async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type, domain } = options;
const { fullRecord,hostRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, domain);
// const domain = await this.matchDomain(fullRecord);
const rr = fullRecord.replace('.' + domain, '');
const params = {
RegionId: 'cn-hangzhou',
DomainName: domain,
RR: rr,
RR: hostRecord,
Type: type,
Value: value,
// Line: 'oversea' // 海外

View File

@@ -1,14 +1,15 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import fs from 'fs';
import path from 'path';
import dayjs from 'dayjs';
import { AbstractPlusTaskPlugin } from '@certd/plugin-plus';
import JSZip from 'jszip';
import * as os from 'node:os';
import { SshAccess, SshClient } from '@certd/plugin-lib';
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import fs from "fs";
import path from "path";
import dayjs from "dayjs";
import { AbstractPlusTaskPlugin } from "@certd/plugin-plus";
import JSZip from "jszip";
import * as os from "node:os";
import { OssClientContext, ossClientFactory, OssClientRemoveByOpts, SshAccess, SshClient } from "@certd/plugin-lib";
const defaultBackupDir = 'certd_backup';
const defaultFilePrefix = 'db-backup';
const defaultFilePrefix = 'db_backup';
@IsTaskPlugin({
name: 'DBBackupPlugin',
title: '数据库备份',
@@ -30,8 +31,9 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
component: {
name: 'a-select',
options: [
{ label: '本地复制', value: 'local' },
{ label: 'ssh上传', value: 'ssh' },
{label: '本地复制', value: 'local'},
{label: 'ssh上传', value: 'ssh'},
{label: 'oss上传', value: 'oss'},
],
placeholder: '',
},
@@ -57,6 +59,53 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
})
sshAccessId!: number;
@TaskInput({
title: 'OSS类型',
component: {
name: 'a-select',
options: [
{value: "alioss", label: "阿里云OSS"},
{value: "s3", label: "MinIO/S3"},
{value: "qiniuoss", label: "七牛云"},
{value: "tencentcos", label: "腾讯云COS"},
{value: "ftp", label: "Ftp"},
{value: "sftp", label: "Sftp"},
]
},
mergeScript: `
return {
show:ctx.compute(({form})=>{
return form.backupMode === 'oss';
})
}
`,
required: true,
})
ossType!: string;
@TaskInput({
title: 'OSS授权',
component: {
name: 'access-selector',
},
mergeScript: `
return {
show:ctx.compute(({form})=>{
return form.backupMode === 'oss';
}),
component:{
type: ctx.compute(({form})=>{
return form.ossType;
}),
}
}
`,
required: true,
})
ossAccessId!: number;
@TaskInput({
title: '备份保存目录',
component: {
@@ -104,7 +153,9 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
})
retainDays!: number;
async onInstance() {}
async onInstance() {
}
async execute(): Promise<void> {
this.logger.info('开始备份数据库');
@@ -114,11 +165,11 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
this.logger.error('数据库文件不存在:', dbPath);
return;
}
const dbTmpFilename = `${this.filePrefix}.${dayjs().format('YYYYMMDD.HHmmss')}.sqlite`;
const dbTmpFilename = `${this.filePrefix}_${dayjs().format('YYYYMMDD_HHmmss')}_sqlite`;
const dbZipFilename = `${dbTmpFilename}.zip`;
const tempDir = path.resolve(os.tmpdir(), 'certd_backup');
if (!fs.existsSync(tempDir)) {
await fs.promises.mkdir(tempDir, { recursive: true });
await fs.promises.mkdir(tempDir, {recursive: true});
}
const dbTmpPath = path.resolve(tempDir, dbTmpFilename);
const dbZipPath = path.resolve(tempDir, dbZipFilename);
@@ -129,14 +180,14 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
const zip = new JSZip();
const stream = fs.createReadStream(dbTmpPath);
// 使用流的方式添加文件内容
zip.file(dbTmpFilename, stream, { binary: true, compression: 'DEFLATE' });
zip.file(dbTmpFilename, stream, {binary: true, compression: 'DEFLATE'});
const uploadDir = path.resolve('data', 'upload');
if (this.withUpload && fs.existsSync(uploadDir)) {
zip.folder(uploadDir);
}
const content = await zip.generateAsync({ type: 'nodebuffer' });
const content = await zip.generateAsync({type: 'nodebuffer'});
await fs.promises.writeFile(dbZipPath, content);
this.logger.info(`数据库文件压缩完成:${dbZipPath}`);
@@ -164,7 +215,7 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
}
const dir = path.dirname(backupPath);
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true });
await fs.promises.mkdir(dir, {recursive: true});
}
backupPath = path.resolve(backupPath);
await fs.promises.copyFile(dbPath, backupPath);
@@ -195,7 +246,7 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
this.logger.info('备份目录:', backupPath);
await sshClient.uploadFiles({
connectConf: access,
transports: [{ localPath: dbPath, remotePath: backupPath }],
transports: [{localPath: dbPath, remotePath: backupPath}],
mkdirs: true,
});
this.logger.info('备份文件上传完成');
@@ -221,7 +272,39 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
}
private async ossBackup(dbPath: string, backupDir: string, backupPath: string) {
// TODO
if (!this.ossAccessId) {
throw new Error('未配置ossAccessId');
}
const access = await this.getAccess(this.ossAccessId);
const ossType = this.ossType
const ctx: OssClientContext = {
logger: this.logger,
utils: this.ctx.utils,
accessService:this.accessService
}
this.logger.info(`开始备份文件到:${ossType}`);
const client = await ossClientFactory.createOssClientByType(ossType, {
access,
ctx,
})
await client.upload(backupPath, dbPath);
if (this.retainDays > 0) {
// 删除过期备份
this.logger.info('开始删除过期备份文件');
const removeByOpts: OssClientRemoveByOpts = {
dir: backupDir,
beforeDays: this.retainDays,
};
await client.removeBy(removeByOpts);
this.logger.info('删除过期备份文件完成');
}else{
this.logger.info('已禁止删除过期文件');
}
}
}
new DBBackupPlugin();

View File

@@ -2,3 +2,4 @@ export * from './plugin-deploy-to-cdn.js'
export * from './plugin-deploy-to-clb.js'
export * from './plugin-upload-to-cert-center.js'
export * from './plugin-deploy-to-alb.js'
export * from './plugin-deploy-to-live.js'

View File

@@ -0,0 +1,144 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { VolcengineAccess } from "../access.js";
import { VolcengineClient } from "../ve-client.js";
@IsTaskPlugin({
name: "VolcengineDeployToLive",
title: "火山引擎-部署证书至Live",
icon: "svg:icon-volcengine",
group: pluginGroups.volcengine.key,
desc: "部署至火山引擎视频直播",
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
export class VolcengineDeployToLive extends AbstractTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
},
required: true
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: "Access授权",
helper: "火山引擎AccessKeyId、AccessKeySecret",
component: {
name: "access-selector",
type: "volcengine"
},
required: true
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: "直播域名",
helper: "选择要部署证书的直播域名",
action: VolcengineDeployToLive.prototype.onGetDomainList.name,
watches: ["certDomains", "accessId"],
required: true
})
)
domainList!: string | string[];
async onInstance() {
}
async execute(): Promise<void> {
this.logger.info("开始部署证书到火山引擎视频直播");
const service = await this.getLiveService();
let certId = await this.uploadCert(service);
for (const item of this.domainList) {
this.logger.info(`开始部署直播域名${item}证书`);
await service.request({
action: "BindCert",
body: {
Domain: item,
HTTPS: true,
ChainID: certId
}
});
this.logger.info(`部署直播域名${item}证书成功`);
}
this.logger.info("部署完成");
}
private async uploadCert(liveService: any) {
const res = await liveService.request({
action: "CreateCert",
body: {
Rsa: {
Pubkey: this.cert.crt,
Prikey: this.cert.key
},
UseWay: "https"
}
});
const certId = res.Result.ChainID;
this.logger.info("证书上传成功", certId);
return certId;
}
private async getLiveService() {
const access = await this.getAccess<VolcengineAccess>(this.accessId);
const client = new VolcengineClient({
logger: this.logger,
access,
http: this.http
});
return await client.getLiveService();
}
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error("请选择Access授权");
}
const service = await this.getLiveService();
const res = await service.request({
action: "ListDomainDetail",
body: {
"PageNum": 1,
"PageSize": 100
}
});
const list = res.Result?.DomainList;
if (!list || list.length === 0) {
throw new Error("找不到直播域名,您也可以手动输入域名");
}
const options = list.map((item: any) => {
return {
value: item.Domain,
label: `${item.Domain}<${item.Type}>`,
domain: item.Domain
};
});
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
}
}
new VolcengineDeployToLive();

View File

@@ -0,0 +1,199 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { VolcengineAccess } from "../access.js";
import { VolcengineClient } from "../ve-client.js";
@IsTaskPlugin({
name: "VolcengineDeployToVOD",
title: "火山引擎-部署证书至VOD",
icon: "svg:icon-volcengine",
group: pluginGroups.volcengine.key,
desc: "部署至火山引擎视频点播(暂不可用)",
deprecated:"暂时缺少部署ssl接口",
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
export class VolcengineDeployToVOD extends AbstractTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames, "VolcengineUploadToCertCenter"]
},
required: true
})
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: "Access授权",
helper: "火山引擎AccessKeyId、AccessKeySecret",
component: {
name: "access-selector",
type: "volcengine"
},
required: true
})
accessId!: string;
@TaskInput(
// createRemoteSelectInputDefine({
// title: "空间名称",
// helper: "选择要部署证书的监听器\n需要在监听器中选择证书中心进行跨服务访问授权",
// action: VolcengineDeployToVOD.prototype.onGetSpaceList.name,
// watches: ["certDomains", "accessId", "regionId"],
// required: true
// })
{
title: "空间名称",
required: true
}
)
spaceName!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: "点播域名",
helper: "选择要部署证书的点播域名\n需要先在域名管理页面进行证书中心访问授权即点击去配置SSL证书",
action: VolcengineDeployToVOD.prototype.onGetDomainList.name,
watches: ["certDomains", "accessId", "spaceName"],
required: true
})
)
domainList!: string | string[];
async onInstance() {
}
async execute(): Promise<void> {
this.logger.info("开始部署证书到火山引擎VOD");
const access = await this.getAccess<VolcengineAccess>(this.accessId);
let certId = await this.uploadOrGetCertId(access);
const service = await this.getVodService();
for (const item of this.domainList) {
this.logger.info(`开始部署点播域名${item}证书`);
await service.request({
action: "ModifyListenerAttributes",
query: {
ListenerId: item,
CertificateSource: "cert_center",
CertCenterCertificateId: certId
}
});
this.logger.info(`部署点播域名${item}证书成功`);
}
this.logger.info("部署完成");
}
private async uploadOrGetCertId(access: VolcengineAccess) {
const certService = await this.getCertService(access);
let certId = this.cert;
if (typeof certId !== "string") {
const certInfo = this.cert as CertInfo;
this.logger.info(`开始上传证书`);
certId = await certService.ImportCertificate({
certName: this.appendTimeSuffix("certd"),
cert: certInfo
});
this.logger.info(`上传证书成功:${certId}`);
} else {
this.logger.info(`使用已有证书ID:${certId}`);
}
return certId;
}
private async getCertService(access: VolcengineAccess) {
const client = new VolcengineClient({
logger: this.logger,
access,
http: this.http
});
return await client.getCertCenterService();
}
private async getVodService(req?: { version?: string }) {
const access = await this.getAccess<VolcengineAccess>(this.accessId);
const client = new VolcengineClient({
logger: this.logger,
access,
http: this.http
});
return await client.getVodService(req);
}
// async onGetSpaceList(data: any) {
// if (!this.accessId) {
// throw new Error("请选择Access授权");
// }
// const service = await this.getVodService();
//
// const res = await service.request({
// action: "ListSpace",
// method: "GET",
// query: {
// PageSize: 100,
// },
// });
//
// const list = res.Result;
// if (!list || list.length === 0) {
// throw new Error("找不到空间,您可以手动填写");
// }
// return list.map((item: any) => {
// return {
// value: item.SpaceName,
// label: `${item.SpaceName}`
// };
// });
// }
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error("请选择Access授权");
}
const service = await this.getVodService();
const res = await service.request({
action: "ListDomain",
body: {
SpaceName: this.spaceName,
Offset: 100
}
});
const instances = res.Result?.PlayInstanceInfo?.ByteInstances;
if (!instances || instances.length === 0) {
throw new Error("找不到点播域名,您也可以手动输入点播域名");
}
const list = []
for (const item of instances) {
for (const domain of item.Domains) {
list.push({
value: item.Domain,
label: item.Domain,
domain: domain.Domain
});
}
}
return this.ctx.utils.options.buildGroupOptions(list, this.certDomains);
}
}
new VolcengineDeployToVOD();

View File

@@ -28,7 +28,7 @@ export class VolcengineClient {
service.ImportCertificate = async (body: { certName: string, cert: any }) => {
const { certName, cert } = body;
const res = await service.request({
const res = await service.request({
action: "ImportCertificate",
method: "POST",
body: {
@@ -40,7 +40,7 @@ export class VolcengineClient {
}
}
});
return res.Result.InstanceId || res.Result.RepeatId
return res.Result.InstanceId || res.Result.RepeatId;
};
return service;
}
@@ -59,6 +59,33 @@ export class VolcengineClient {
return service;
}
async getLiveService() {
const CommonService = await this.getServiceCls();
const service = new CommonService({
serviceName: "live",
defaultVersion: "2023-01-01"
});
service.setAccessKeyId(this.opts.access.accessKeyId);
service.setSecretKey(this.opts.access.secretAccessKey);
service.setRegion("cn-north-1");
return service;
}
async getVodService(opts?: { version?: string }) {
const CommonService = await this.getServiceCls();
const service = new CommonService({
serviceName: "vod",
defaultVersion: opts?.version || "2021-01-01"
});
service.setAccessKeyId(this.opts.access.accessKeyId);
service.setSecretKey(this.opts.access.secretAccessKey);
return service;
}
async getAlbService(opts: { region?: string }) {
const CommonService = await this.getServiceCls();

View File

@@ -0,0 +1,136 @@
import CryptoJS from 'crypto-js'
function aes(val) {
var k = CryptoJS.enc.Utf8.parse('1234567890abcDEF');
var iv = CryptoJS.enc.Utf8.parse('1234567890abcDEF');
const enc = CryptoJS.AES.encrypt(val, k, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
}).toString();
return enc;
}
import axios from 'axios'
const instance = axios.create({
baseURL: 'https://www.51dns.com',
timeout: 5000,
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
async function login() {
const res = await instance.request({
url: 'https://www.51dns.com/login.html',
method: 'get',
headers: {
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36',
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com',
}
})
//提取 var csrfToken = "ieOfM21eDd9nWJv3OZtMJF6ogDsnPKQHJ17dlMck";
const _token = res.data.match(/var csrfToken = "(.*?)"/)[1]
console.log(_token)
console.log(res.headers)
const setCookie = res.headers['set-cookie']
const cookie = setCookie.map(item => {
return item.split(';')[0]
}).join(';')
var obj = {
'email_or_phone': aes(""),
'password': aes(""),
'type': aes('account'),
'redirectTo': 'https://www.51dns.com/domain',
'_token': _token
}
console.log(JSON.stringify(obj, null, 2))
const res2 = await instance.request({
url: 'https://www.51dns.com/login',
method: 'post',
data: {
...obj
},
headers: {
/**
* Origin:
* https://www.51dns.com
* Referer:
* https://www.51dns.com/login.html
* User-Agent:
* Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36
// __root_domain_v=.51dns.com;
*/
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36',
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com/login.html',
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': cookie,
//X-Requested-With:
// XMLHttpRequest
'X-Requested-With': 'XMLHttpRequest'
}
})
console.log(res2.headers)
if (res2.data.code == 0) {
console.log("登录成功")
}
const setCookie2 = res2.headers['set-cookie']
const cookie2 = setCookie2.map(item => {
return item.split(';')[0]
}).join(';')
//
// // console.log(res2.data)
// // 提取 <span class="user_email">182****43522</span><br>
// console.log(res2.data.match(/<span class="user_email">(.*?)<\/span>/)[1])
// const success1 = res2.data.includes('<span class="nav-title">DNS解析</span>')
// console.log("success", success1)
const res3 = await instance.request({
url: 'https://www.51dns.com/domain',
method: 'get',
withCredentials: true,
headers: {
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36',
'Origin': 'https://www.51dns.com',
'Referer': 'https://www.51dns.com/login.html',
'Cookie': cookie2,
}
})
console.log(res3.statusText)
console.log(res3.headers)
const success2 = res3.data.includes('<span class="nav-title">DNS解析</span>')
console.log("success", success2)
/**
* <a target="_blank" href="https://www.51dns.com/domain/record/193341603"
* class="color47">certd.top</a>
*/
//上面文本中间有换行,需要提取 193341603 部分,必须有certd.top,使用 new Regexp, .号要能匹配换行符,非贪婪模式
const regExp = new RegExp('<a target="_blank" href="https://www.51dns.com/domain/record/(\\d+)"[^>]*>certd\\.top<\\/a>',"i");
const domainId = res3.data.match(regExp)[1]
console.log("domainId", domainId)
}
login()

1240
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff