mirror of
https://github.com/certd/certd.git
synced 2026-07-01 08:57:33 +08:00
perf: 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip
自定义插件需要压缩包时可以调用new CertReader(certInfo).buildZip() 方式获取
This commit is contained in:
@@ -77,6 +77,20 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
|
||||
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
|
||||
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 务必写单元测试,覆盖主要业务逻辑。
|
||||
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
|
||||
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
||||
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
||||
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。
|
||||
- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。
|
||||
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。
|
||||
- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`。
|
||||
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
|
||||
|
||||
|
||||
## 后端规则
|
||||
|
||||
- 后端主包是 `packages/ui/certd-server`,使用 Node.js、ESM、TypeScript、MidwayJS 3、Koa、TypeORM 和 SQL 迁移。
|
||||
@@ -196,13 +210,3 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。
|
||||
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
|
||||
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
||||
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
||||
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。
|
||||
- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。
|
||||
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。
|
||||
- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`。
|
||||
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { AppKey, PlusRequestService } from '@certd/plus-core';
|
||||
import { cache, http, HttpRequestConfig, logger } from '@certd/basic';
|
||||
import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from '../../settings/index.js';
|
||||
import { merge } from 'lodash-es';
|
||||
import fs from 'fs';
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { AppKey, PlusRequestService } from "@certd/plus-core";
|
||||
import { cache, http, HttpRequestConfig, logger } from "@certd/basic";
|
||||
import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from "../../settings/index.js";
|
||||
import { merge } from "lodash-es";
|
||||
import fs from "fs";
|
||||
@Provide("plusService")
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class PlusService {
|
||||
@@ -54,9 +54,9 @@ export class PlusService {
|
||||
await plusRequestService.verify({ license: licenseInfo.license });
|
||||
}
|
||||
|
||||
async bindUrl(url: string, url2?:string) {
|
||||
async bindUrl(url: string, url2?: string) {
|
||||
const plusRequestService = await this.getPlusRequestService();
|
||||
const res = await plusRequestService.bindUrl(url,url2);
|
||||
const res = await plusRequestService.bindUrl(url, url2);
|
||||
this.plusRequestService = null;
|
||||
return res;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export class PlusService {
|
||||
const licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
|
||||
if (!licenseInfo.license) {
|
||||
await plusRequestService.register();
|
||||
logger.info('站点注册成功');
|
||||
logger.info("站点注册成功");
|
||||
this.plusRequestService = null;
|
||||
}
|
||||
}
|
||||
@@ -74,8 +74,8 @@ export class PlusService {
|
||||
async userPreBind(userId: number) {
|
||||
const plusRequestService = await this.getPlusRequestService();
|
||||
await plusRequestService.requestWithoutSign({
|
||||
url: '/activation/subject/preBind',
|
||||
method: 'POST',
|
||||
url: "/activation/subject/preBind",
|
||||
method: "POST",
|
||||
data: {
|
||||
userId,
|
||||
appKey: AppKey,
|
||||
@@ -91,9 +91,9 @@ export class PlusService {
|
||||
if (attachments.length > 0) {
|
||||
const newAttachments: any[] = [];
|
||||
attachments.forEach((item: any) => {
|
||||
const name = item.filename || item.path.split('/').pop();
|
||||
const name = item.filename || item.path.split("/").pop();
|
||||
const body = item.content || fs.readFileSync(item.path);
|
||||
const bodyBase64 = Buffer.from(body).toString('base64');
|
||||
const bodyBase64 = Buffer.from(body).toString("base64");
|
||||
item = {
|
||||
name,
|
||||
body: bodyBase64,
|
||||
@@ -104,7 +104,7 @@ export class PlusService {
|
||||
}
|
||||
|
||||
await plusRequestService.request({
|
||||
url: '/activation/emailSend',
|
||||
url: "/activation/emailSend",
|
||||
data: {
|
||||
subject: email.subject,
|
||||
to: email.receivers,
|
||||
@@ -116,7 +116,7 @@ export class PlusService {
|
||||
}
|
||||
|
||||
async getAccessToken() {
|
||||
const cacheKey = 'certd:subject:access_token';
|
||||
const cacheKey = "certd:subject:access_token";
|
||||
const token = cache.get(cacheKey);
|
||||
if (token) {
|
||||
return token;
|
||||
@@ -131,15 +131,15 @@ export class PlusService {
|
||||
return res.accessToken;
|
||||
}
|
||||
|
||||
async getVipTrial(vipType= "plus") {
|
||||
async getVipTrial(vipType = "plus") {
|
||||
await this.register();
|
||||
const plusRequestService = await this.getPlusRequestService();
|
||||
const res = await plusRequestService.request({
|
||||
url: '/activation/subject/vip/trialGet',
|
||||
method: 'POST',
|
||||
data:{
|
||||
vipType
|
||||
}
|
||||
url: "/activation/subject/vip/trialGet",
|
||||
method: "POST",
|
||||
data: {
|
||||
vipType,
|
||||
},
|
||||
});
|
||||
if (res.license) {
|
||||
await this.updateLicense(res.license);
|
||||
@@ -147,14 +147,14 @@ export class PlusService {
|
||||
duration: res.duration,
|
||||
};
|
||||
} else {
|
||||
throw new Error('您已经领取过VIP试用了');
|
||||
throw new Error("您已经领取过VIP试用了");
|
||||
}
|
||||
}
|
||||
|
||||
async getTodayOrderCount () {
|
||||
async getTodayOrderCount() {
|
||||
await this.register();
|
||||
const plusRequestService = await this.getPlusRequestService();
|
||||
return await plusRequestService.getOrderCount()
|
||||
return await plusRequestService.getOrderCount();
|
||||
}
|
||||
|
||||
async requestWithToken(config: HttpRequestConfig) {
|
||||
@@ -162,7 +162,7 @@ export class PlusService {
|
||||
const token = await this.getAccessToken();
|
||||
merge(config, {
|
||||
baseURL: plusRequestService.getBaseURL(),
|
||||
method: 'post',
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: `Berear ${token}`,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "test/**/*.test.ts",
|
||||
"require": "ts-node/register"
|
||||
"node-option": ["loader=ts-node/esm", "no-warnings"]
|
||||
}
|
||||
@@ -10,10 +10,7 @@
|
||||
"before-build": "rimraf dist && rimraf tsconfig.tsbuildinfo && rimraf .rollup.cache",
|
||||
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
|
||||
"dev-build": "npm run build",
|
||||
"build3": "rollup -c",
|
||||
"build2": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch",
|
||||
"format": "prettier --write src",
|
||||
@@ -24,12 +21,13 @@
|
||||
"@certd/basic": "^1.41.4",
|
||||
"@certd/pipeline": "^1.41.4",
|
||||
"dayjs": "^1.11.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"psl": "^1.15.0",
|
||||
"punycode.js": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^5.0.5",
|
||||
"rimraf": "^5.0.5",
|
||||
"@types/chai": "^4.3.12",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
@@ -41,6 +39,7 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.6.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"prettier": "3.3.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
|
||||
@@ -6,6 +6,7 @@ import cryptoLib from "crypto";
|
||||
import { ILogger } from "@certd/basic";
|
||||
import dayjs from "dayjs";
|
||||
import { uniq } from "lodash-es";
|
||||
import JSZip from "jszip";
|
||||
|
||||
export interface ICertInfoGetter {
|
||||
getByPipelineId: (pipelineId: number) => Promise<CertInfo>;
|
||||
@@ -301,4 +302,70 @@ export class CertReader {
|
||||
static buildCertName(cert: CertInfo, useHash: boolean = false) {
|
||||
return new CertReader(cert).buildCertName("", useHash);
|
||||
}
|
||||
|
||||
async buildZip(): Promise<Buffer> {
|
||||
const cert = this.cert;
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
if (cert.crt) {
|
||||
zip.file("证书.pem", cert.crt);
|
||||
}
|
||||
if (cert.key) {
|
||||
zip.file("私钥.pem", cert.key);
|
||||
}
|
||||
if (cert.ic) {
|
||||
zip.file("中间证书.pem", cert.ic);
|
||||
}
|
||||
if (cert.crt) {
|
||||
zip.file("cert.crt", cert.crt);
|
||||
}
|
||||
if (cert.key) {
|
||||
zip.file("cert.key", cert.key);
|
||||
}
|
||||
if (cert.ic) {
|
||||
zip.file("intermediate.crt", cert.ic);
|
||||
}
|
||||
if (cert.oc) {
|
||||
zip.file("origin.crt", cert.oc);
|
||||
}
|
||||
if (cert.one) {
|
||||
zip.file("one.pem", cert.one);
|
||||
}
|
||||
if (cert.p7b) {
|
||||
zip.file("cert.p7b", cert.p7b);
|
||||
}
|
||||
if (cert.pfx) {
|
||||
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
|
||||
}
|
||||
if (cert.der) {
|
||||
zip.file("cert.der", Buffer.from(cert.der, "base64"));
|
||||
}
|
||||
if (cert.jks) {
|
||||
zip.file("cert.jks", Buffer.from(cert.jks, "base64"));
|
||||
}
|
||||
|
||||
zip.file(
|
||||
"说明.txt",
|
||||
`证书文件说明
|
||||
cert.crt:证书文件,包含证书链,pem格式
|
||||
cert.key:私钥文件,pem格式
|
||||
intermediate.crt:中间证书文件,pem格式
|
||||
origin.crt:原始证书文件,不含证书链,pem格式
|
||||
one.pem: 证书和私钥简单合并成一个文件,pem格式,crt正文+key正文
|
||||
cert.pfx:pfx格式证书文件,iis服务器使用
|
||||
cert.der:der格式证书文件
|
||||
cert.jks:jks格式证书文件,java服务器使用
|
||||
`
|
||||
);
|
||||
|
||||
return zip.generateAsync({ type: "nodebuffer" });
|
||||
}
|
||||
|
||||
buildZipFilename(prefix = "cert"): string {
|
||||
let domain = this.getMainDomain();
|
||||
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
|
||||
const timeStr = dayjs().format("YYYYMMDDHHmmss");
|
||||
return `${prefix}_${domain}_${timeStr}.zip`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/// <reference types="mocha" />
|
||||
/// <reference types="node" />
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { CertReader } from "../src/cert/cert-reader.js";
|
||||
import type { CertInfo } from "../src/cert/cert-reader.js";
|
||||
|
||||
// @ts-ignore
|
||||
import forge from "node-forge";
|
||||
|
||||
/**
|
||||
* Generate a minimal self-signed X.509 cert + key in PEM format for testing.
|
||||
*/
|
||||
function createSelfSignedCert(commonName: string): { crt: string; key: string } {
|
||||
const keypair = forge.pki.rsa.generateKeyPair(2048);
|
||||
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keypair.publicKey;
|
||||
cert.serialNumber = "01";
|
||||
cert.validFrom = new Date("2025-01-01").toISOString();
|
||||
cert.validTo = new Date("2026-01-01").toISOString();
|
||||
|
||||
const attrs = [{ name: "commonName", value: commonName }];
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs);
|
||||
cert.sign(keypair.privateKey, forge.md.sha256.create());
|
||||
|
||||
return {
|
||||
crt: forge.pki.certificateToPem(cert),
|
||||
key: forge.pki.privateKeyToPem(keypair.privateKey),
|
||||
};
|
||||
}
|
||||
|
||||
const testCert = createSelfSignedCert("example.com");
|
||||
const testCertIc = createSelfSignedCert("intermediate.ca");
|
||||
const mockCertInfo: CertInfo = {
|
||||
crt: testCert.crt + "\n" + testCertIc.crt,
|
||||
key: testCert.key,
|
||||
oc: testCert.crt,
|
||||
ic: testCertIc.crt,
|
||||
one: testCert.crt + "\n" + testCert.key,
|
||||
p7b: "PKCS7 test content",
|
||||
pfx: Buffer.from("fake-pfx-data").toString("base64"),
|
||||
der: Buffer.from("fake-der-data").toString("base64"),
|
||||
jks: Buffer.from("fake-jks-data").toString("base64"),
|
||||
};
|
||||
|
||||
describe("CertReader.buildZip", () => {
|
||||
it("returns a non-empty Buffer", async () => {
|
||||
const reader = new CertReader(mockCertInfo);
|
||||
const buf = await reader.buildZip();
|
||||
assert.ok(Buffer.isBuffer(buf));
|
||||
assert.ok(buf.length > 0);
|
||||
});
|
||||
|
||||
it("produces a valid zip containing expected files", async () => {
|
||||
const reader = new CertReader(mockCertInfo);
|
||||
const buf = await reader.buildZip();
|
||||
const { default: JSZip } = await import("jszip");
|
||||
const zip = await JSZip.loadAsync(buf);
|
||||
|
||||
assert.ok(zip.file("证书.pem"), "should contain 证书.pem");
|
||||
assert.ok(zip.file("私钥.pem"), "should contain 私钥.pem");
|
||||
assert.ok(zip.file("中间证书.pem"), "should contain 中间证书.pem");
|
||||
assert.ok(zip.file("cert.crt"), "should contain cert.crt");
|
||||
assert.ok(zip.file("cert.key"), "should contain cert.key");
|
||||
assert.ok(zip.file("intermediate.crt"), "should contain intermediate.crt");
|
||||
assert.ok(zip.file("origin.crt"), "should contain origin.crt");
|
||||
assert.ok(zip.file("one.pem"), "should contain one.pem");
|
||||
assert.ok(zip.file("cert.p7b"), "should contain cert.p7b");
|
||||
assert.ok(zip.file("cert.pfx"), "should contain cert.pfx");
|
||||
assert.ok(zip.file("cert.der"), "should contain cert.der");
|
||||
assert.ok(zip.file("cert.jks"), "should contain cert.jks");
|
||||
assert.ok(zip.file("说明.txt"), "should contain 说明.txt");
|
||||
|
||||
const pemContent = await zip.file("证书.pem").async("string");
|
||||
assert.ok(pemContent.includes("-----BEGIN CERTIFICATE-----"));
|
||||
|
||||
const pfx = await zip.file("cert.pfx").async("nodebuffer");
|
||||
assert.equal(pfx.toString(), "fake-pfx-data");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CertReader.buildZipFilename", () => {
|
||||
it("includes the main domain and timestamp", () => {
|
||||
const reader = new CertReader(mockCertInfo);
|
||||
const name = reader.buildZipFilename("cert");
|
||||
assert.ok(name.startsWith("cert_example_com_"));
|
||||
assert.ok(name.endsWith(".zip"));
|
||||
const tsPart = name.replace("cert_example_com_", "").replace(".zip", "");
|
||||
assert.match(tsPart, /^\d{14}$/);
|
||||
});
|
||||
|
||||
it("uses the default prefix when not provided", () => {
|
||||
const reader = new CertReader(mockCertInfo);
|
||||
const name = reader.buildZipFilename();
|
||||
assert.ok(name.startsWith("cert_example_com_"));
|
||||
});
|
||||
|
||||
it("wildcard domain replaces asterisk", () => {
|
||||
const wildcardCert = createSelfSignedCert("*.example.com");
|
||||
const wcInfo: CertInfo = { crt: wildcardCert.crt, key: wildcardCert.key };
|
||||
const reader = new CertReader(wcInfo);
|
||||
const name = reader.buildZipFilename("cert");
|
||||
assert.ok(name.startsWith("cert___example_com_"), "asterisk should be replaced, got: " + name);
|
||||
assert.ok(!name.includes("*"));
|
||||
});
|
||||
});
|
||||
@@ -138,10 +138,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: "link",
|
||||
icon: "ant-design:download-outlined",
|
||||
async click({ row }) {
|
||||
if (!row.certFile) {
|
||||
notification.error({ message: t("certd.certificateNotGenerated") });
|
||||
return;
|
||||
}
|
||||
let url = "/api/monitor/cert/download?id=" + row.id;
|
||||
if (projectStore.isEnterprise) {
|
||||
url += `&projectId=${projectStore.currentProject?.id}`;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as api from "/@/views/certd/pipeline/api";
|
||||
import { notification } from "ant-design-vue";
|
||||
import CertView from "/@/views/certd/pipeline/cert-view.vue";
|
||||
import { env } from "/@/utils/util.env";
|
||||
import { useModal } from "/@/use/use-modal";
|
||||
import { useProjectStore } from "/@/store/project";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import * as api from "/@/views/certd/pipeline/api";
|
||||
|
||||
export function useCertViewer() {
|
||||
const projectStore = useProjectStore();
|
||||
@@ -29,42 +29,12 @@ export function useCertViewer() {
|
||||
};
|
||||
|
||||
const downloadCert = async (id: any) => {
|
||||
const files = await api.GetFiles(id);
|
||||
model.success({
|
||||
title: "点击链接下载",
|
||||
maskClosable: true,
|
||||
okText: "关闭",
|
||||
content: () => {
|
||||
const children = [];
|
||||
for (const file of files) {
|
||||
let downloadUrl = `${env.API}/pi/history/download?pipelineId=${id}&fileId=${file.id}`;
|
||||
if (projectStore.isEnterprise) {
|
||||
downloadUrl += `&projectId=${projectStore.currentProject?.id}`;
|
||||
}
|
||||
downloadUrl += `&token=${userStore.getToken}`;
|
||||
children.push(
|
||||
<div>
|
||||
<div class={"flex-o m-5"}>
|
||||
<fs-icon icon={"ant-design:cloud-download-outlined"} class={"mr-5 fs-16"}></fs-icon>
|
||||
<a href={downloadUrl} target={"_blank"}>
|
||||
{file.filename}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (children.length === 0) {
|
||||
return <div>暂无文件下载</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={"mt-3"}>
|
||||
<div> {children}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
let downloadUrl = `${env.API}/pi/cert/downloadZip?id=${id}`;
|
||||
if (projectStore.isEnterprise) {
|
||||
downloadUrl += `&projectId=${projectStore.currentProject?.id}`;
|
||||
}
|
||||
downloadUrl += `&token=${userStore.getToken}`;
|
||||
window.open(downloadUrl);
|
||||
};
|
||||
return {
|
||||
viewCert,
|
||||
|
||||
@@ -556,9 +556,6 @@ output:
|
||||
cert:
|
||||
title: 域名证书
|
||||
type: cert
|
||||
certZip:
|
||||
title: 域名证书压缩文件
|
||||
type: certZip
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/apply.js
|
||||
|
||||
@@ -146,9 +146,6 @@ output:
|
||||
cert:
|
||||
title: 域名证书
|
||||
type: cert
|
||||
certZip:
|
||||
title: 域名证书压缩文件
|
||||
type: certZip
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/getter/aliyun.js
|
||||
|
||||
@@ -168,9 +168,6 @@ output:
|
||||
cert:
|
||||
title: 域名证书
|
||||
type: cert
|
||||
certZip:
|
||||
title: 域名证书压缩文件
|
||||
type: certZip
|
||||
pluginType: deploy
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/lego/index.js
|
||||
|
||||
@@ -142,9 +142,6 @@ output:
|
||||
cert:
|
||||
title: 域名证书
|
||||
type: cert
|
||||
certZip:
|
||||
title: 域名证书压缩文件
|
||||
type: certZip
|
||||
certMd5:
|
||||
title: 证书MD5
|
||||
type: certMd5
|
||||
|
||||
@@ -17,15 +17,6 @@ input:
|
||||
- ':cert:'
|
||||
required: true
|
||||
order: 0
|
||||
certZip:
|
||||
title: 证书压缩文件
|
||||
helper: 请选择前置任务输出的域名证书压缩文件
|
||||
component:
|
||||
name: output-selector
|
||||
from:
|
||||
- ':certZip:'
|
||||
required: true
|
||||
order: 0
|
||||
email:
|
||||
title: 接收邮箱
|
||||
component:
|
||||
|
||||
@@ -131,7 +131,6 @@ export class MainConfiguration {
|
||||
setLogger((text: string) => {
|
||||
logger.info(text);
|
||||
});
|
||||
|
||||
logger.info("当前环境:", this.app.getEnv()); // prod
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { CertInfoService } from "../../../modules/monitor/index.js";
|
||||
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
|
||||
import { SelectQueryBuilder } from "typeorm";
|
||||
import { logger } from "@certd/basic";
|
||||
import fs from "fs";
|
||||
import dayjs from "dayjs";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { CertReader } from "@certd/plugin-lib";
|
||||
@@ -167,29 +166,29 @@ export class CertInfoController extends CrudController<CertInfoService> {
|
||||
@Get("/download", { description: Constants.per.authOnly, summary: "下载证书文件" })
|
||||
async download(@Query("id") id: number) {
|
||||
const { userId, projectId } = await this.checkOwner(this.getService(), id, "read");
|
||||
const certInfo = await this.getService().info(id);
|
||||
if (certInfo == null) {
|
||||
const certInfoEntity = await this.getService().info(id);
|
||||
if (certInfoEntity == null) {
|
||||
throw new CommonException("file not found");
|
||||
}
|
||||
if (certInfo.userId !== userId) {
|
||||
if (certInfoEntity.userId !== userId) {
|
||||
throw new CommonException("file not found");
|
||||
}
|
||||
if (projectId && certInfo.projectId !== projectId) {
|
||||
if (projectId && certInfoEntity.projectId !== projectId) {
|
||||
throw new CommonException("file not found");
|
||||
}
|
||||
// koa send file
|
||||
// 下载文件的名称
|
||||
// const filename = file.filename;
|
||||
// 要下载的文件的完整路径
|
||||
const path = certInfo.certFile;
|
||||
if (!path) {
|
||||
throw new CommonException("file not found");
|
||||
if (!certInfoEntity.certInfo) {
|
||||
throw new CommonException("证书数据未生成");
|
||||
}
|
||||
logger.info(`download:${path}`);
|
||||
// 以流的形式下载文件
|
||||
this.ctx.attachment(path);
|
||||
this.ctx.set("Content-Type", "application/octet-stream");
|
||||
|
||||
return fs.createReadStream(path);
|
||||
const certInfo = JSON.parse(certInfoEntity.certInfo);
|
||||
const certReader = new CertReader(certInfo);
|
||||
const zipBuffer = await certReader.buildZip();
|
||||
const filename = certReader.buildZipFilename("cert");
|
||||
|
||||
logger.info(`download cert zip: ${filename}, size: ${zipBuffer.length}`);
|
||||
|
||||
this.ctx.attachment(filename);
|
||||
this.ctx.set("Content-Type", "application/zip");
|
||||
this.ctx.body = zipBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
|
||||
import { BaseController, Constants, PermissionException } from "@certd/lib-server";
|
||||
import { StorageService } from "../../../modules/pipeline/service/storage-service.js";
|
||||
@@ -6,6 +6,7 @@ import { CertReader } from "@certd/plugin-cert";
|
||||
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
|
||||
import { UserGrantSetting } from "../../../modules/mine/service/models.js";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { logger } from "@certd/basic";
|
||||
|
||||
@Provide()
|
||||
@Controller("/api/pi/cert")
|
||||
@@ -57,4 +58,38 @@ export class CertController extends BaseController {
|
||||
const certDetail = CertReader.readCertDetail(crt);
|
||||
return this.ok(certDetail);
|
||||
}
|
||||
|
||||
@Get("/downloadZip", { description: Constants.per.authOnly, summary: "下载流水线证书压缩包" })
|
||||
async downloadZip(@Query("id") id: number) {
|
||||
const { userId } = await this.getProjectUserIdRead();
|
||||
|
||||
const pipelineUserId = await this.pipelineService.getPipelineUserId(id);
|
||||
|
||||
if (pipelineUserId !== userId) {
|
||||
const isAdmin = await this.isAdmin();
|
||||
if (!isAdmin) {
|
||||
throw new PermissionException();
|
||||
}
|
||||
const setting = await this.userSettingsService.getSetting<UserGrantSetting>(pipelineUserId, null, UserGrantSetting, false);
|
||||
if (setting?.allowAdminViewCerts !== true) {
|
||||
throw new PermissionException("该流水线的用户还未授权管理员下载证书,请先让用户在”设置->授权委托“中打开开关");
|
||||
}
|
||||
}
|
||||
|
||||
const privateVars = await this.storeService.getPipelinePrivateVars(id);
|
||||
const certInfo = privateVars.cert;
|
||||
if (!certInfo?.crt) {
|
||||
throw new Error("该流水线还未生成证书,请先运行一次流水线");
|
||||
}
|
||||
|
||||
const certReader = new CertReader(certInfo);
|
||||
const zipBuffer = await certReader.buildZip();
|
||||
const filename = certReader.buildZipFilename("cert");
|
||||
|
||||
logger.info(`download pipeline cert zip: ${filename}, size: ${zipBuffer.length}`);
|
||||
|
||||
this.ctx.attachment(filename);
|
||||
this.ctx.set("Content-Type", "application/zip");
|
||||
this.ctx.body = zipBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-58
@@ -1,8 +1,7 @@
|
||||
import { AbstractTaskPlugin, FileItem, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { CertConverter, CertReader, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-lib";
|
||||
import dayjs from "dayjs";
|
||||
import type { CertInfo } from "./acme.js";
|
||||
import { CertReader, CertConverter, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-lib";
|
||||
import JSZip from "jszip";
|
||||
|
||||
export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
@@ -72,12 +71,6 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
cert?: CertInfo;
|
||||
|
||||
@TaskOutput({
|
||||
title: "域名证书压缩文件",
|
||||
type: "certZip",
|
||||
})
|
||||
certZip?: FileItem;
|
||||
|
||||
async onInstance() {
|
||||
this.userContext = this.ctx.userContext;
|
||||
this.lastStatus = this.ctx.lastStatus as Step;
|
||||
@@ -108,7 +101,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
|
||||
}
|
||||
this._result.pipelinePrivateVars.cert = cert;
|
||||
|
||||
if (isNew) {
|
||||
if (isNew || !cert.pfx) {
|
||||
try {
|
||||
const converter = new CertConverter({ logger: this.logger });
|
||||
const res = await converter.convert({
|
||||
@@ -137,54 +130,6 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
|
||||
this.logger.error("转换证书格式失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore);
|
||||
await this.zipCert(cert, zipFileName);
|
||||
} else {
|
||||
this.extendsFiles();
|
||||
}
|
||||
this.certZip = this._result.files[0];
|
||||
}
|
||||
|
||||
async zipCert(cert: CertInfo, filename: string) {
|
||||
const zip = new JSZip();
|
||||
zip.file("证书.pem", cert.crt);
|
||||
zip.file("私钥.pem", cert.key);
|
||||
zip.file("中间证书.pem", cert.ic);
|
||||
zip.file("cert.crt", cert.crt);
|
||||
zip.file("cert.key", cert.key);
|
||||
zip.file("intermediate.crt", cert.ic);
|
||||
zip.file("origin.crt", cert.oc);
|
||||
zip.file("one.pem", cert.one);
|
||||
zip.file("cert.p7b", cert.p7b);
|
||||
if (cert.pfx) {
|
||||
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
|
||||
}
|
||||
if (cert.der) {
|
||||
zip.file("cert.der", Buffer.from(cert.der, "base64"));
|
||||
}
|
||||
if (cert.jks) {
|
||||
zip.file("cert.jks", Buffer.from(cert.jks, "base64"));
|
||||
}
|
||||
|
||||
zip.file(
|
||||
"说明.txt",
|
||||
`证书文件说明
|
||||
cert.crt:证书文件,包含证书链,pem格式
|
||||
cert.key:私钥文件,pem格式
|
||||
intermediate.crt:中间证书文件,pem格式
|
||||
origin.crt:原始证书文件,不含证书链,pem格式
|
||||
one.pem: 证书和私钥简单合并成一个文件,pem格式,crt正文+key正文
|
||||
cert.pfx:pfx格式证书文件,iis服务器使用
|
||||
cert.der:der格式证书文件
|
||||
cert.jks:jks格式证书文件,java服务器使用
|
||||
`
|
||||
);
|
||||
|
||||
const content = await zip.generateAsync({ type: "nodebuffer" });
|
||||
this.saveFile(filename, content);
|
||||
this.logger.info(`已保存文件:${filename}`);
|
||||
}
|
||||
|
||||
formatCert(pem: string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractTaskPlugin, FileItem, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import dayjs from "dayjs";
|
||||
import { get } from "lodash-es";
|
||||
@@ -28,17 +28,6 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: "证书压缩文件",
|
||||
helper: "请选择前置任务输出的域名证书压缩文件",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [":certZip:"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
certZip!: FileItem;
|
||||
|
||||
@TaskInput({
|
||||
title: "接收邮箱",
|
||||
component: {
|
||||
@@ -146,18 +135,18 @@ export class DeployCertToMailPlugin extends AbstractTaskPlugin {
|
||||
`;
|
||||
data.content = content;
|
||||
data.title = title;
|
||||
const file = this.certZip;
|
||||
if (!file) {
|
||||
throw new Error("证书压缩文件还未生成,重新运行证书任务");
|
||||
}
|
||||
|
||||
const zipBuffer = await certReader.buildZip();
|
||||
const zipFilename = certReader.buildZipFilename("cert");
|
||||
|
||||
await this.ctx.emailService.sendByTemplate({
|
||||
type: "sendCert",
|
||||
data,
|
||||
receivers: this.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: file.filename,
|
||||
path: file.path,
|
||||
filename: zipFilename,
|
||||
content: zipBuffer,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user