perf: 支持http校验方式申请证书

This commit is contained in:
xiaojunnuo
2025-01-02 00:28:13 +08:00
parent 67af67b92d
commit 405591c5d0
42 changed files with 820 additions and 74 deletions

View File

@@ -18,8 +18,8 @@
"@alicloud/pop-core": "^1.7.10",
"@certd/basic": "^1.29.2",
"@certd/pipeline": "^1.29.2",
"@certd/plugin-cert": "^1.29.2",
"@kubernetes/client-node": "0.21.0",
"basic-ftp": "^5.0.5",
"dayjs": "^1.11.7",
"iconv-lite": "^0.6.3",
"lodash-es": "^4.17.21",

View File

@@ -0,0 +1,71 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "alioss",
title: "阿里云OSS授权",
desc: "包含地域和Bucket",
icon: "ant-design:aliyun-outlined",
})
export class AliossAccess extends BaseAccess {
@AccessInput({
title: "阿里云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "aliyun",
},
helper: "请选择阿里云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "大区",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "oss-cn-hangzhou", label: "华东1杭州" },
{ value: "oss-cn-shanghai", label: "华东2上海" },
{ value: "oss-cn-nanjing", label: "华东5南京-本地地域)" },
{ value: "oss-cn-fuzhou", label: "华东6福州-本地地域)" },
{ value: "oss-cn-wuhan-lr", label: "华中1武汉-本地地域)" },
{ value: "oss-cn-qingdao", label: "华北1青岛" },
{ value: "oss-cn-beijing", label: "华北2北京" },
{ value: "oss-cn-zhangjiakou", label: "华北 3张家口" },
{ value: "oss-cn-huhehaote", label: "华北5呼和浩特" },
{ value: "oss-cn-wulanchabu", label: "华北6乌兰察布" },
{ value: "oss-cn-shenzhen", label: "华南1深圳" },
{ value: "oss-cn-heyuan", label: "华南2河源" },
{ value: "oss-cn-guangzhou", label: "华南3广州" },
{ value: "oss-cn-chengdu", label: "西南1成都" },
{ value: "oss-cn-hongkong", label: "中国香港" },
{ value: "oss-us-west-1", label: "美国(硅谷)①" },
{ value: "oss-us-east-1", label: "美国(弗吉尼亚)①" },
{ value: "oss-ap-northeast-1", label: "日本(东京)①" },
{ value: "oss-ap-northeast-2", label: "韩国(首尔)" },
{ value: "oss-ap-southeast-1", label: "新加坡①" },
{ value: "oss-ap-southeast-2", label: "澳大利亚(悉尼)①" },
{ value: "oss-ap-southeast-3", label: "马来西亚(吉隆坡)①" },
{ value: "oss-ap-southeast-5", label: "印度尼西亚(雅加达)①" },
{ value: "oss-ap-southeast-6", label: "菲律宾(马尼拉)" },
{ value: "oss-ap-southeast-7", label: "泰国(曼谷)" },
{ value: "oss-eu-central-1", label: "德国(法兰克福)①" },
{ value: "oss-eu-west-1", label: "英国(伦敦)" },
{ value: "oss-me-east-1", label: "阿联酋(迪拜)①" },
{ value: "oss-rg-china-mainland", label: "无地域属性(中国内地)" },
],
},
required: true,
})
region!: string;
@AccessInput({
title: "Bucket",
helper: "存储桶名称",
required: true,
})
bucket!: string;
}
new AliossAccess();

View File

@@ -1 +1,2 @@
export * from './aliyun-access.js';
export * from "./aliyun-access.js";
export * from "./alioss-access.js";

View File

@@ -0,0 +1,56 @@
import { AliyunAccess } from "../access";
export class AliossClient {
access: AliyunAccess;
region: string;
bucket: string;
client: any;
constructor(opts: { access: AliyunAccess; bucket: string; region: string }) {
this.access = opts.access;
this.bucket = opts.bucket;
this.region = opts.region;
}
async init() {
// @ts-ignore
const OSS = await import("ali-oss");
this.client = new OSS.default({
accessKeyId: this.access.accessKeyId,
accessKeySecret: this.access.accessKeySecret,
// yourRegion填写Bucket所在地域。以华东1杭州为例Region填写为oss-cn-hangzhou。
region: this.region,
//@ts-ignore
authorizationV4: true,
// yourBucketName填写Bucket名称。
bucket: this.bucket,
});
}
async doRequest(client: any, bucket: string, xml: string, params: any) {
params = client._bucketRequestParams("POST", bucket, {
...params,
});
params.content = xml;
params.mime = "xml";
params.successStatuses = [200];
const res = await client.request(params);
this.checkRet(res);
return res;
}
checkRet(ret: any) {
if (ret.code != null) {
throw new Error("执行失败:" + ret.Message);
}
}
async uploadFile(filePath: string, content: Buffer) {
const memFile = new File([content], filePath);
return await this.client.put(filePath, memFile);
}
async removeFile(filePath: string) {
return await this.client.delete(filePath);
}
}

View File

@@ -1,8 +1,11 @@
import { ILogger } from "@certd/basic";
import { AliyunAccess } from "../access/index.js";
import { AliyunClient } from "./index.js";
import { CertInfo } from "@certd/plugin-cert";
export type AliyunCertInfo = {
crt: string; //fullchain证书
key: string; //私钥
};
export type AliyunSslClientOpts = {
access: AliyunAccess;
logger: ILogger;
@@ -23,7 +26,7 @@ export type AliyunSslCreateDeploymentJobReq = {
export type AliyunSslUploadCertReq = {
name: string;
cert: CertInfo;
cert: AliyunCertInfo;
};
export class AliyunSslClient {

View File

@@ -0,0 +1,77 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: "ftp",
title: "FTP授权",
desc: "",
icon: "mdi:folder-upload-outline",
})
export class FtpAccess extends BaseAccess {
@AccessInput({
title: "host",
component: {
placeholder: "ip / 域名",
name: "a-input",
vModel: "value",
},
helper: "FTP地址",
required: true,
})
host!: string;
@AccessInput({
title: "host",
value: 21,
component: {
placeholder: "21",
name: "a-input-number",
vModel: "value",
},
helper: "FTP端口",
required: true,
})
port!: string;
@AccessInput({
title: "user",
component: {
placeholder: "用户名",
},
helper: "FTP用户名",
required: true,
})
user!: string;
@AccessInput({
title: "password",
component: {
placeholder: "密码",
component: {
name: "a-input-password",
vModel: "value",
},
},
encrypt: true,
helper: "FTP密码",
required: true,
})
password!: string;
@AccessInput({
title: "secure",
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "是否使用SSL",
required: true,
})
secure?: boolean = false;
}
new FtpAccess();

View File

@@ -0,0 +1,47 @@
import { FtpAccess } from "./access";
import { ILogger } from "@certd/basic";
import path from "node:path";
export class FtpClient {
access: FtpAccess = null;
logger: ILogger = null;
client: any;
constructor(opts: { access: FtpAccess; logger: ILogger }) {
this.access = opts.access;
this.logger = opts.logger;
}
async connect(callback: (client: FtpClient) => Promise<void>) {
const ftp = await import("basic-ftp");
const Client = ftp.Client;
const client = new Client();
client.ftp.verbose = true;
this.logger.info("开始连接FTP");
await client.access(this.access as any);
this.logger.info("FTP连接成功");
this.client = client;
try {
await callback(this);
} finally {
if (client) {
client.close();
}
}
}
async upload(filePath: string, remotePath: string): Promise<void> {
if (!remotePath) {
return;
}
const dirname = path.dirname(remotePath);
this.logger.info(`确保目录存在:${dirname}`);
await this.client.ensureDir(dirname);
this.logger.info(`开始上传文件${filePath} -> ${remotePath}`);
await this.client.uploadFrom(filePath, remotePath);
}
async remove(filePath: string): Promise<void> {
this.logger.info(`开始删除文件${filePath}`);
await this.client.remove(filePath, true);
}
}

View File

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

View File

@@ -1,3 +1,6 @@
export * from "./ssh/index.js";
export * from "./aliyun/index.js";
export * from "./common/index.js";
export * from "./ftp/index.js";
export * from "./tencent/index.js";
export * from "./qiniu/index.js";

View File

@@ -0,0 +1,31 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "qiniuoss",
title: "七牛OSS授权",
desc: "",
icon: "svg:icon-qiniuyun",
input: {},
})
export class QiniuOssAccess extends BaseAccess {
@AccessInput({
title: "七牛云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "qiniu",
},
helper: "请选择七牛云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "Bucket",
helper: "存储桶名称",
required: true,
})
bucket = "";
}
new QiniuOssAccess();

View File

@@ -0,0 +1,25 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "qiniu",
title: "七牛云授权",
desc: "",
icon: "svg:icon-qiniuyun",
input: {},
})
export class QiniuAccess extends BaseAccess {
@AccessInput({
title: "AccessKey",
rules: [{ required: true, message: "此项必填" }],
helper: "AK前往[密钥管理](https://portal.qiniu.com/developer/user/key)获取",
})
accessKey!: string;
@AccessInput({
title: "SecretKey",
encrypt: true,
helper: "SK",
})
secretKey!: string;
}
new QiniuAccess();

View File

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

View File

@@ -7,6 +7,7 @@ import { SshAccess } from "./ssh-access.js";
import stripAnsi from "strip-ansi";
import { SocksClient } from "socks";
import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js";
export type TransportItem = { localPath: string; remotePath: string };
export class AsyncSsh2Client {
conn: ssh2.Client;
@@ -95,6 +96,21 @@ export class AsyncSsh2Client {
});
}
async unlink(options: { sftp: any; remotePath: string }) {
const { sftp, remotePath } = options;
return new Promise((resolve, reject) => {
this.logger.info(`开始删除远程文件:${remotePath}`);
sftp.unlink(remotePath, (err: Error) => {
if (err) {
reject(err);
return;
}
this.logger.info(`删除文件成功:${remotePath}`);
resolve({});
});
});
}
async exec(
script: string,
opts: {
@@ -239,7 +255,7 @@ export class SshClient {
}
* @param options
*/
async uploadFiles(options: { connectConf: SshAccess; transports: any; mkdirs: boolean }) {
async uploadFiles(options: { connectConf: SshAccess; transports: TransportItem[]; mkdirs: boolean }) {
const { connectConf, transports, mkdirs } = options;
await this._call({
connectConf,
@@ -272,6 +288,24 @@ export class SshClient {
});
}
async removeFiles(opts: { connectConf: SshAccess; files: string[] }) {
const { connectConf, files } = opts;
await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {
const sftp = await conn.getSftp();
this.logger.info("开始删除");
for (const file of files) {
await conn.unlink({
sftp,
remotePath: file,
});
}
this.logger.info("文件全部删除成功");
},
});
}
async isCmd(conn: AsyncSsh2Client) {
const spec = await conn.exec("echo %COMSPEC% ");
if (spec.toString().trim() === "%COMSPEC%") {

View File

@@ -0,0 +1,63 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "tencentcos",
title: "腾讯云COS授权",
icon: "svg:icon-tencentcloud",
desc: "腾讯云对象存储授权,包含地域和存储桶",
})
export class TencentCosAccess extends BaseAccess {
@AccessInput({
title: "腾讯云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "tencent",
},
helper: "请选择腾讯云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "所在地域",
helper: "存储桶所在地域",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "", label: "--------中国大陆地区-------", disabled: true },
{ value: "ap-beijing-1", label: "北京1区" },
{ value: "ap-beijing", label: "北京" },
{ value: "ap-nanjing", label: "南京" },
{ value: "ap-shanghai", label: "上海" },
{ value: "ap-guangzhou", label: "广州" },
{ value: "ap-chengdu", label: "成都" },
{ value: "ap-chongqing", label: "重庆" },
{ value: "ap-shenzhen-fsi", label: "深圳金融" },
{ value: "ap-shanghai-fsi", label: "上海金融" },
{ value: "ap-beijing-fsi", label: "北京金融" },
{ value: "", label: "--------中国香港及境外-------", disabled: true },
{ value: "ap-hongkong", label: "中国香港" },
{ value: "ap-singapore", label: "新加坡" },
{ value: "ap-mumbai", label: "孟买" },
{ value: "ap-jakarta", label: "雅加达" },
{ value: "ap-seoul", label: "首尔" },
{ value: "ap-bangkok", label: "曼谷" },
{ value: "ap-tokyo", label: "东京" },
{ value: "na-siliconvalley", label: "硅谷" },
{ value: "na-ashburn", label: "弗吉尼亚" },
{ value: "sa-saopaulo", label: "圣保罗" },
{ value: "eu-frankfurt", label: "法兰克福" },
],
},
})
region!: string;
@AccessInput({
title: "Bucket",
helper: "存储桶名称",
required: true,
})
bucket = "";
}

View File

@@ -0,0 +1,28 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
@IsAccess({
name: "tencent",
title: "腾讯云",
icon: "svg:icon-tencentcloud",
})
export class TencentAccess extends BaseAccess {
@AccessInput({
title: "secretId",
helper:
"使用对应的插件需要有对应的权限,比如上传证书,需要证书管理权限;部署到clb需要clb相关权限\n前往[密钥管理](https://console.cloud.tencent.com/cam/capi)进行创建",
component: {
placeholder: "secretId",
},
rules: [{ required: true, message: "该项必填" }],
})
secretId = "";
@AccessInput({
title: "secretKey",
component: {
placeholder: "secretKey",
},
encrypt: true,
rules: [{ required: true, message: "该项必填" }],
})
secretKey = "";
}

View File

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