mirror of
https://github.com/certd/certd.git
synced 2026-04-20 17:57:41 +08:00
feat: 【破坏性更新】插件改为metadata加载模式,plugin-cert、plugin-lib包部分代码转移到certd-server中,影响自定义插件,需要修改相关import引用
ssh、aliyun、tencent、qiniu、oss等 access和client需要转移import
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "aliesa",
|
||||
title: "阿里云ESA授权",
|
||||
desc: "",
|
||||
icon: "ant-design:aliyun-outlined",
|
||||
order: 0,
|
||||
})
|
||||
export class AliesaAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "阿里云授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
vModel: "modelValue",
|
||||
type: "aliyun",
|
||||
},
|
||||
helper: "请选择阿里云授权",
|
||||
required: true,
|
||||
})
|
||||
accessId = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "地区",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{
|
||||
label: "杭州",
|
||||
value: "cn-hangzhou",
|
||||
},
|
||||
{
|
||||
label: "新加坡",
|
||||
value: "ap-southeast-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
helper: "请选择ESA地区",
|
||||
required: true,
|
||||
})
|
||||
region = "";
|
||||
}
|
||||
|
||||
new AliesaAccess();
|
||||
@@ -1,71 +0,0 @@
|
||||
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();
|
||||
@@ -1,129 +0,0 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { ILogger } from "@certd/basic";
|
||||
export type AliyunClientV2Req = {
|
||||
action: string;
|
||||
version: string;
|
||||
protocol?: "HTTPS";
|
||||
// 接口 HTTP 方法
|
||||
method?: "GET" | "POST";
|
||||
authType?: "AK";
|
||||
style?: "RPC" | "ROA";
|
||||
// 接口 PATH
|
||||
pathname?: string;
|
||||
|
||||
data?: any;
|
||||
};
|
||||
export class AliyunClientV2 {
|
||||
access: AliyunAccess;
|
||||
logger: ILogger;
|
||||
endpoint: string;
|
||||
|
||||
client: any;
|
||||
constructor(opts: { access: AliyunAccess; logger: ILogger; endpoint: string }) {
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
this.endpoint = opts.endpoint;
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
if (this.client) {
|
||||
return this.client;
|
||||
}
|
||||
const $OpenApi = await import("@alicloud/openapi-client");
|
||||
// const Credential = await import("@alicloud/credentials");
|
||||
// //@ts-ignore
|
||||
// const credential = new Credential.default.default({
|
||||
//
|
||||
// type: "access_key",
|
||||
// });
|
||||
const config = new $OpenApi.Config({
|
||||
accessKeyId: this.access.accessKeyId,
|
||||
accessKeySecret: this.access.accessKeySecret,
|
||||
});
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/FC
|
||||
// config.endpoint = `esa.${this.regionId}.aliyuncs.com`;
|
||||
config.endpoint = this.endpoint;
|
||||
//@ts-ignore
|
||||
this.client = new $OpenApi.default.default(config);
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async doRequest(req: AliyunClientV2Req) {
|
||||
const client = await this.getClient();
|
||||
|
||||
const $OpenApi = await import("@alicloud/openapi-client");
|
||||
const $Util = await import("@alicloud/tea-util");
|
||||
const OpenApiUtil = await import("@alicloud/openapi-util");
|
||||
const params = new $OpenApi.Params({
|
||||
// 接口名称
|
||||
action: req.action,
|
||||
// 接口版本
|
||||
version: req.version,
|
||||
// 接口协议
|
||||
protocol: "HTTPS",
|
||||
// 接口 HTTP 方法
|
||||
method: req.method ?? "POST",
|
||||
authType: req.authType ?? "AK",
|
||||
style: req.style ?? "RPC",
|
||||
// 接口 PATH
|
||||
pathname: req.pathname ?? `/`,
|
||||
// 接口请求体内容格式
|
||||
reqBodyType: "json",
|
||||
// 接口响应体内容格式
|
||||
bodyType: "json",
|
||||
});
|
||||
|
||||
if (req.data?.query) {
|
||||
//@ts-ignore
|
||||
req.data.query = OpenApiUtil.default.default.query(req.data.query);
|
||||
}
|
||||
const runtime = new $Util.RuntimeOptions({});
|
||||
const request = new $OpenApi.OpenApiRequest(req.data);
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
// 返回值实际为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
|
||||
const res = await client.callApi(params, request, runtime);
|
||||
/**
|
||||
* res?.body?.
|
||||
*/
|
||||
return res?.body;
|
||||
}
|
||||
}
|
||||
|
||||
@IsAccess({
|
||||
name: "aliyun",
|
||||
title: "阿里云授权",
|
||||
desc: "",
|
||||
icon: "ant-design:aliyun-outlined",
|
||||
order: 0,
|
||||
})
|
||||
export class AliyunAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "accessKeyId",
|
||||
component: {
|
||||
placeholder: "accessKeyId",
|
||||
},
|
||||
helper: "登录阿里云控制台->AccessKey管理页面获取。",
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = "";
|
||||
@AccessInput({
|
||||
title: "accessKeySecret",
|
||||
component: {
|
||||
placeholder: "accessKeySecret",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
helper: "注意:证书申请需要dns解析权限;其他阿里云插件,需要对应的权限,比如证书上传需要证书管理权限;嫌麻烦就用主账号的全量权限的accessKey",
|
||||
})
|
||||
accessKeySecret = "";
|
||||
|
||||
getClient(endpoint: string) {
|
||||
return new AliyunClientV2({
|
||||
access: this,
|
||||
logger: this.ctx.logger,
|
||||
endpoint: endpoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new AliyunAccess();
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./aliyun-access.js";
|
||||
export * from "./alioss-access.js";
|
||||
export * from "./aliesa-access.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./lib/index.js";
|
||||
export * from "./access/index.js";
|
||||
@@ -1,78 +0,0 @@
|
||||
import { getGlobalAgents, ILogger } from "@certd/basic";
|
||||
|
||||
export class AliyunClient {
|
||||
client: any;
|
||||
logger: ILogger;
|
||||
agent: any;
|
||||
useROAClient: boolean;
|
||||
|
||||
constructor(opts: { logger: ILogger; useROAClient?: boolean }) {
|
||||
this.logger = opts.logger;
|
||||
this.useROAClient = opts.useROAClient || false;
|
||||
const agents = getGlobalAgents();
|
||||
this.agent = agents.httpsAgent;
|
||||
}
|
||||
|
||||
async getSdk() {
|
||||
if (this.useROAClient) {
|
||||
return await this.getROAClient();
|
||||
}
|
||||
const Core = await import("@alicloud/pop-core");
|
||||
return Core.default;
|
||||
}
|
||||
|
||||
async getROAClient() {
|
||||
const Core = await import("@alicloud/pop-core");
|
||||
console.log("aliyun sdk", Core);
|
||||
// @ts-ignore
|
||||
return Core.ROAClient;
|
||||
}
|
||||
|
||||
async init(opts: any) {
|
||||
const Core = await this.getSdk();
|
||||
this.client = new Core(opts);
|
||||
return this.client;
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (ret.Code != null && ret.Code !== "OK" && ret.Message !== "OK") {
|
||||
throw new Error("执行失败:" + ret.Message);
|
||||
}
|
||||
}
|
||||
|
||||
async request(
|
||||
name: string,
|
||||
params: any,
|
||||
requestOption: any = {
|
||||
method: "POST",
|
||||
formatParams: false,
|
||||
}
|
||||
) {
|
||||
if (!this.useROAClient) {
|
||||
requestOption.agent = this.agent;
|
||||
}
|
||||
|
||||
const getNumberFromEnv = (key: string, defValue: number) => {
|
||||
const value = process.env[key];
|
||||
if (value) {
|
||||
try {
|
||||
return parseInt(value);
|
||||
} catch (e: any) {
|
||||
this.logger.error(`环境变量${key}设置错误,应该是一个数字,当前值为${value},将使用默认值:${defValue}`);
|
||||
return defValue;
|
||||
}
|
||||
} else {
|
||||
return defValue;
|
||||
}
|
||||
};
|
||||
|
||||
// 连接超时设置,仅对当前请求有效。
|
||||
requestOption.connectTimeout = getNumberFromEnv("ALIYUN_CLIENT_CONNECT_TIMEOUT", 8000);
|
||||
// 读超时设置,仅对当前请求有效。
|
||||
requestOption.readTimeout = getNumberFromEnv("ALIYUN_CLIENT_READ_TIMEOUT", 8000);
|
||||
|
||||
const res = await this.client.request(name, params, requestOption);
|
||||
this.checkRet(res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./base-client.js";
|
||||
export * from "./ssl-client.js";
|
||||
export * from "./oss-client.js";
|
||||
@@ -1,87 +0,0 @@
|
||||
import { AliyunAccess } from "../access/index.js";
|
||||
|
||||
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() {
|
||||
if (this.client) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const OSS = await import("ali-oss");
|
||||
const ossClient = 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,
|
||||
});
|
||||
// oss
|
||||
|
||||
this.client = ossClient;
|
||||
}
|
||||
|
||||
async doRequest(bucket: string, xml: string, params: any) {
|
||||
await this.init();
|
||||
params = this.client._bucketRequestParams("POST", bucket, {
|
||||
...params,
|
||||
});
|
||||
params.content = xml;
|
||||
params.mime = "xml";
|
||||
params.successStatuses = [200];
|
||||
const res = await this.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 | string, timeout = 1000 * 60 * 60) {
|
||||
await this.init();
|
||||
return await this.client.put(filePath, content, {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async removeFile(filePath: string) {
|
||||
await this.init();
|
||||
return await this.client.delete(filePath);
|
||||
}
|
||||
|
||||
async downloadFile(key: string, savePath: string, timeout = 1000 * 60 * 60) {
|
||||
await this.init();
|
||||
return await this.client.get(key, savePath, {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import { ILogger } from "@certd/basic";
|
||||
import { AliyunAccess } from "../access/index.js";
|
||||
import { AliyunClient } from "./index.js";
|
||||
|
||||
export type AliyunCertInfo = {
|
||||
crt: string; //fullchain证书
|
||||
key: string; //私钥
|
||||
};
|
||||
export type AliyunSslClientOpts = {
|
||||
access: AliyunAccess;
|
||||
logger: ILogger;
|
||||
endpoint?: string;
|
||||
region?: string;
|
||||
};
|
||||
|
||||
export type AliyunSslGetResourceListReq = {
|
||||
cloudProduct: string;
|
||||
};
|
||||
|
||||
export type AliyunSslCreateDeploymentJobReq = {
|
||||
name: string;
|
||||
jobType: string;
|
||||
contactIds: string[];
|
||||
resourceIds: string[];
|
||||
certIds: string[];
|
||||
};
|
||||
|
||||
export type AliyunSslUploadCertReq = {
|
||||
name: string;
|
||||
cert: AliyunCertInfo;
|
||||
};
|
||||
|
||||
export type CasCertInfo = { certId: number; certName: string; certIdentifier: string; notAfter: number; casRegion: string };
|
||||
|
||||
export class AliyunSslClient {
|
||||
opts: AliyunSslClientOpts;
|
||||
logger: ILogger;
|
||||
constructor(opts: AliyunSslClientOpts) {
|
||||
this.opts = opts;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (ret.Code != null) {
|
||||
throw new Error("执行失败:" + ret.Message);
|
||||
}
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const access = this.opts.access;
|
||||
const client = new AliyunClient({ logger: this.opts.logger });
|
||||
|
||||
let endpoint = this.opts.endpoint || "cas.aliyuncs.com";
|
||||
if (this.opts.endpoint == null && this.opts.region) {
|
||||
if (this.opts.region === "cn-hangzhou") {
|
||||
endpoint = "cas.aliyuncs.com";
|
||||
} else {
|
||||
endpoint = `cas.${this.opts.region}.aliyuncs.com`;
|
||||
}
|
||||
}
|
||||
await client.init({
|
||||
accessKeyId: access.accessKeyId,
|
||||
accessKeySecret: access.accessKeySecret,
|
||||
endpoint: `https://${endpoint}`,
|
||||
apiVersion: "2020-04-07",
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
async getCertInfo(certId: number): Promise<CasCertInfo> {
|
||||
const client = await this.getClient();
|
||||
const params = {
|
||||
CertId: certId,
|
||||
};
|
||||
|
||||
const res = await client.request("GetUserCertificateDetail", params);
|
||||
this.checkRet(res);
|
||||
|
||||
return {
|
||||
certId: certId,
|
||||
certName: res.Name,
|
||||
certIdentifier: res.CertIdentifier,
|
||||
notAfter: res.NotAfter,
|
||||
casRegion: this.getCasRegionFromEndpoint(this.opts.endpoint),
|
||||
};
|
||||
}
|
||||
|
||||
async uploadCert(req: AliyunSslUploadCertReq) {
|
||||
const client = await this.getClient();
|
||||
const params = {
|
||||
Name: req.name,
|
||||
Cert: req.cert.crt,
|
||||
Key: req.cert.key,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
this.opts.logger.info(`开始上传证书:${req.name}`);
|
||||
const ret: any = await client.request("UploadUserCertificate", params, requestOption);
|
||||
this.checkRet(ret);
|
||||
this.opts.logger.info("证书上传成功:aliyunCertId=", ret.CertId);
|
||||
//output
|
||||
return ret.CertId;
|
||||
}
|
||||
|
||||
async getResourceList(req: AliyunSslGetResourceListReq) {
|
||||
const client = await this.getClient();
|
||||
const params = {
|
||||
CloudName: "aliyun",
|
||||
CloudProduct: req.cloudProduct,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
formatParams: false,
|
||||
};
|
||||
|
||||
const res = await client.request("ListCloudResources", params, requestOption);
|
||||
this.checkRet(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async createDeploymentJob(req: AliyunSslCreateDeploymentJobReq) {
|
||||
const client = await this.getClient();
|
||||
|
||||
const params = {
|
||||
Name: req.name,
|
||||
JobType: req.jobType,
|
||||
ContactIds: req.contactIds.join(","),
|
||||
ResourceIds: req.resourceIds.join(","),
|
||||
CertIds: req.certIds.join(","),
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
formatParams: false,
|
||||
};
|
||||
|
||||
const res = await client.request("CreateDeploymentJob", params, requestOption);
|
||||
this.checkRet(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async getContactList() {
|
||||
const params = {};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
formatParams: false,
|
||||
};
|
||||
const client = await this.getClient();
|
||||
const res = await client.request("ListContact", params, requestOption);
|
||||
this.checkRet(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async doRequest(action: string, params: any, requestOption: any) {
|
||||
const client = await this.getClient();
|
||||
const res = await client.request(action, params, requestOption);
|
||||
this.checkRet(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async deleteCert(certId: any) {
|
||||
await this.doRequest("DeleteUserCertificate", { CertId: certId }, { method: "POST" });
|
||||
}
|
||||
|
||||
getCasRegionFromEndpoint(endpoint: string) {
|
||||
if (!endpoint) {
|
||||
return "cn-hangzhou";
|
||||
}
|
||||
/**
|
||||
* {value: 'cas.aliyuncs.com', label: '中国大陆'},
|
||||
* {value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡'},
|
||||
* {value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)'},
|
||||
*/
|
||||
const region = endpoint.replace(".aliyuncs.com", "").replace("cas.", "");
|
||||
if (region === "cas") {
|
||||
return "cn-hangzhou";
|
||||
}
|
||||
return region;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { CertificateInfo, crypto } from "@certd/acme-client";
|
||||
import { ILogger } from "@certd/basic";
|
||||
import dayjs from "dayjs";
|
||||
import { uniq } from "lodash-es";
|
||||
|
||||
export type CertInfo = {
|
||||
crt: string; //fullchain证书
|
||||
key: string; //私钥
|
||||
csr: string; //csr
|
||||
oc?: string; //仅证书,非fullchain证书
|
||||
ic?: string; //中间证书
|
||||
pfx?: string;
|
||||
der?: string;
|
||||
jks?: string;
|
||||
one?: string;
|
||||
p7b?: string;
|
||||
};
|
||||
|
||||
export type CertReaderHandleContext = {
|
||||
reader: CertReader;
|
||||
tmpCrtPath: string;
|
||||
tmpKeyPath: string;
|
||||
tmpOcPath?: string;
|
||||
tmpPfxPath?: string;
|
||||
tmpDerPath?: string;
|
||||
tmpIcPath?: string;
|
||||
tmpJksPath?: string;
|
||||
tmpOnePath?: string;
|
||||
tmpP7bPath?: string;
|
||||
};
|
||||
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
|
||||
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
|
||||
|
||||
const formats = {
|
||||
pem: ["crt", "key", "ic"],
|
||||
one: ["one"],
|
||||
pfx: ["pfx"],
|
||||
der: ["der"],
|
||||
jks: ["jks"],
|
||||
p7b: ["p7b", "key"],
|
||||
};
|
||||
export class CertReader {
|
||||
cert: CertInfo;
|
||||
|
||||
detail: CertificateInfo;
|
||||
//毫秒时间戳
|
||||
effective: number;
|
||||
//毫秒时间戳
|
||||
expires: number;
|
||||
constructor(certInfo: CertInfo) {
|
||||
this.cert = certInfo;
|
||||
|
||||
if (!certInfo.ic) {
|
||||
this.cert.ic = this.getIc();
|
||||
}
|
||||
|
||||
if (!certInfo.oc) {
|
||||
this.cert.oc = this.getOc();
|
||||
}
|
||||
|
||||
if (!certInfo.one) {
|
||||
this.cert.one = this.cert.crt + "\n" + this.cert.key;
|
||||
}
|
||||
|
||||
try {
|
||||
const { detail, effective, expires } = this.getCrtDetail(this.cert.crt);
|
||||
this.detail = detail;
|
||||
this.effective = effective.getTime();
|
||||
this.expires = expires.getTime();
|
||||
} catch (e) {
|
||||
throw new Error("证书解析失败:" + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
getIc() {
|
||||
//中间证书ic, 就是crt的第一个 -----END CERTIFICATE----- 之后的内容
|
||||
const endStr = "-----END CERTIFICATE-----";
|
||||
const firstBlockEndIndex = this.cert.crt.indexOf(endStr);
|
||||
|
||||
const start = firstBlockEndIndex + endStr.length + 1;
|
||||
if (this.cert.crt.length <= start) {
|
||||
return "";
|
||||
}
|
||||
const ic = this.cert.crt.substring(start);
|
||||
if (ic == null) {
|
||||
return "";
|
||||
}
|
||||
return ic?.trim();
|
||||
}
|
||||
|
||||
getOc() {
|
||||
//原始证书 就是crt的第一个 -----END CERTIFICATE----- 之前的内容
|
||||
const endStr = "-----END CERTIFICATE-----";
|
||||
const arr = this.cert.crt.split(endStr);
|
||||
return arr[0] + endStr;
|
||||
}
|
||||
|
||||
toCertInfo(format?: string): CertInfo {
|
||||
if (!format) {
|
||||
return this.cert;
|
||||
}
|
||||
|
||||
const formatArr = formats[format];
|
||||
const res: any = {};
|
||||
formatArr.forEach((key: string) => {
|
||||
res[key] = this.cert[key];
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
getCrtDetail(crt: string = this.cert.crt) {
|
||||
return CertReader.readCertDetail(crt);
|
||||
}
|
||||
|
||||
static readCertDetail(crt: string) {
|
||||
const detail = crypto.readCertificateInfo(crt.toString());
|
||||
const effective = detail.notBefore;
|
||||
const expires = detail.notAfter;
|
||||
return { detail, effective, expires };
|
||||
}
|
||||
|
||||
getAllDomains() {
|
||||
const { detail } = this.getCrtDetail();
|
||||
const domains = [];
|
||||
if (detail.domains?.commonName) {
|
||||
domains.push(detail.domains.commonName);
|
||||
}
|
||||
domains.push(...detail.domains.altNames);
|
||||
//去重
|
||||
return uniq(domains);
|
||||
}
|
||||
|
||||
getAltNames() {
|
||||
const { detail } = this.getCrtDetail();
|
||||
return detail.domains.altNames;
|
||||
}
|
||||
|
||||
static getMainDomain(crt: string) {
|
||||
const { detail } = CertReader.readCertDetail(crt);
|
||||
return CertReader.getMainDomainFromDetail(detail);
|
||||
}
|
||||
|
||||
getMainDomain() {
|
||||
const { detail } = this.getCrtDetail();
|
||||
return CertReader.getMainDomainFromDetail(detail);
|
||||
}
|
||||
|
||||
static getMainDomainFromDetail(detail: CertificateInfo) {
|
||||
let domain = detail?.domains?.commonName;
|
||||
if (domain == null) {
|
||||
domain = detail?.domains?.altNames?.[0];
|
||||
}
|
||||
if (domain == null) {
|
||||
domain = "unknown";
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks" | "p7b", filepath?: string) {
|
||||
if (!this.cert[type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filepath == null) {
|
||||
//写入临时目录
|
||||
filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.${type}`);
|
||||
}
|
||||
|
||||
const dir = path.dirname(filepath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one" || type === "p7b") {
|
||||
fs.writeFileSync(filepath, this.cert[type]);
|
||||
} else {
|
||||
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
|
||||
async readCertFile(opts: HandleOpts) {
|
||||
const logger = opts.logger;
|
||||
logger.info("将证书写入本地缓存文件");
|
||||
const tmpCrtPath = this.saveToFile("crt");
|
||||
const tmpKeyPath = this.saveToFile("key");
|
||||
const tmpPfxPath = this.saveToFile("pfx");
|
||||
const tmpIcPath = this.saveToFile("ic");
|
||||
const tmpOcPath = this.saveToFile("oc");
|
||||
const tmpDerPath = this.saveToFile("der");
|
||||
const tmpJksPath = this.saveToFile("jks");
|
||||
const tmpOnePath = this.saveToFile("one");
|
||||
const tmpP7bPath = this.saveToFile("p7b");
|
||||
logger.info("本地文件写入成功");
|
||||
try {
|
||||
return await opts.handle({
|
||||
reader: this,
|
||||
tmpCrtPath,
|
||||
tmpKeyPath,
|
||||
tmpPfxPath,
|
||||
tmpDerPath,
|
||||
tmpIcPath,
|
||||
tmpJksPath,
|
||||
tmpOcPath,
|
||||
tmpP7bPath,
|
||||
tmpOnePath,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("处理失败", err);
|
||||
throw err;
|
||||
} finally {
|
||||
//删除临时文件
|
||||
logger.info("清理临时文件");
|
||||
function removeFile(filepath?: string) {
|
||||
if (filepath) {
|
||||
fs.unlinkSync(filepath);
|
||||
}
|
||||
}
|
||||
removeFile(tmpCrtPath);
|
||||
removeFile(tmpKeyPath);
|
||||
removeFile(tmpPfxPath);
|
||||
removeFile(tmpOcPath);
|
||||
removeFile(tmpDerPath);
|
||||
removeFile(tmpIcPath);
|
||||
removeFile(tmpJksPath);
|
||||
removeFile(tmpOnePath);
|
||||
removeFile(tmpP7bPath);
|
||||
}
|
||||
}
|
||||
|
||||
buildCertFileName(suffix: string, applyTime: any, prefix = "cert") {
|
||||
let domain = this.getMainDomain();
|
||||
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
|
||||
const timeStr = dayjs(applyTime).format("YYYYMMDDHHmmss");
|
||||
return `${prefix}_${domain}_${timeStr}.${suffix}`;
|
||||
}
|
||||
|
||||
buildCertName(prefix: string = "") {
|
||||
let domain = this.getMainDomain();
|
||||
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
|
||||
return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
|
||||
}
|
||||
|
||||
static appendTimeSuffix(name?: string) {
|
||||
if (name == null) {
|
||||
name = "certd";
|
||||
}
|
||||
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
|
||||
}
|
||||
|
||||
static buildCertName(cert: any) {
|
||||
return new CertReader(cert).buildCertName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const CertApplyPluginNames = [":cert:"];
|
||||
export const EVENT_CERT_APPLY_SUCCESS = "CertApply.success";
|
||||
@@ -0,0 +1,147 @@
|
||||
import { ILogger, sp } from "@certd/basic";
|
||||
import type { CertInfo } from "./cert-reader.js";
|
||||
import { CertReader, CertReaderHandleContext } from "./cert-reader.js";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import fs from "fs";
|
||||
|
||||
export class CertConverter {
|
||||
logger: ILogger;
|
||||
|
||||
constructor(opts: { logger: ILogger }) {
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
async convert(opts: { cert: CertInfo; pfxPassword: string; pfxArgs: string }): Promise<{
|
||||
pfx: string;
|
||||
der: string;
|
||||
jks: string;
|
||||
p7b: string;
|
||||
}> {
|
||||
const certReader = new CertReader(opts.cert);
|
||||
let pfx: string;
|
||||
let der: string;
|
||||
let jks: string;
|
||||
let p7b: string;
|
||||
const handle = async (ctx: CertReaderHandleContext) => {
|
||||
// 调用openssl 转pfx
|
||||
pfx = await this.convertPfx(ctx, opts.pfxPassword, opts.pfxArgs);
|
||||
|
||||
// 转der
|
||||
der = await this.convertDer(ctx);
|
||||
|
||||
jks = await this.convertJks(ctx, opts.pfxPassword);
|
||||
|
||||
p7b = await this.convertP7b(ctx);
|
||||
};
|
||||
|
||||
await certReader.readCertFile({ logger: this.logger, handle });
|
||||
|
||||
return {
|
||||
pfx,
|
||||
der,
|
||||
jks,
|
||||
p7b,
|
||||
};
|
||||
}
|
||||
|
||||
async exec(cmd: string) {
|
||||
process.env.LANG = "zh_CN.GBK";
|
||||
await sp.spawn({
|
||||
cmd: cmd,
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
private async convertPfx(opts: CertReaderHandleContext, pfxPassword: string, pfxArgs: string) {
|
||||
const { tmpCrtPath, tmpKeyPath } = opts;
|
||||
|
||||
const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "_cert.pfx");
|
||||
|
||||
const dir = path.dirname(pfxPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
let passwordArg = "-passout pass:";
|
||||
if (pfxPassword) {
|
||||
passwordArg = `-password pass:${pfxPassword}`;
|
||||
}
|
||||
// 兼容server 2016,旧版本不能用sha256
|
||||
const oldPfxCmd = `openssl pkcs12 ${pfxArgs} -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`;
|
||||
// const newPfx = `openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`;
|
||||
await this.exec(oldPfxCmd);
|
||||
const fileBuffer = fs.readFileSync(pfxPath);
|
||||
const pfxCert = fileBuffer.toString("base64");
|
||||
fs.unlinkSync(pfxPath);
|
||||
return pfxCert;
|
||||
|
||||
//
|
||||
// const applyTime = new Date().getTime();
|
||||
// const filename = reader.buildCertFileName("pfx", applyTime);
|
||||
// this.saveFile(filename, fileBuffer);
|
||||
}
|
||||
|
||||
private async convertDer(opts: CertReaderHandleContext) {
|
||||
const { tmpCrtPath } = opts;
|
||||
const derPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.der`);
|
||||
|
||||
const dir = path.dirname(derPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`);
|
||||
const fileBuffer = fs.readFileSync(derPath);
|
||||
const derCert = fileBuffer.toString("base64");
|
||||
fs.unlinkSync(derPath);
|
||||
return derCert;
|
||||
}
|
||||
|
||||
async convertP7b(opts: CertReaderHandleContext) {
|
||||
const { tmpCrtPath } = opts;
|
||||
const p7bPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.p7b`);
|
||||
const dir = path.dirname(p7bPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
//openssl crl2pkcs7 -nocrl \
|
||||
// -certfile your_domain.crt \
|
||||
// -certfile intermediate.crt \
|
||||
// -out chain.p7b
|
||||
await this.exec(`openssl crl2pkcs7 -nocrl -certfile ${tmpCrtPath} -out ${p7bPath}`);
|
||||
const fileBuffer = fs.readFileSync(p7bPath);
|
||||
const p7bCert = fileBuffer.toString();
|
||||
fs.unlinkSync(p7bPath);
|
||||
return p7bCert;
|
||||
}
|
||||
async convertJks(opts: CertReaderHandleContext, pfxPassword = "") {
|
||||
const jksPassword = pfxPassword || "123456";
|
||||
try {
|
||||
const randomStr = Math.floor(Math.random() * 1000000) + "";
|
||||
|
||||
const p12Path = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.p12`);
|
||||
const { tmpCrtPath, tmpKeyPath } = opts;
|
||||
let passwordArg = "-passout pass:";
|
||||
if (jksPassword) {
|
||||
passwordArg = `-password pass:${jksPassword}`;
|
||||
}
|
||||
await this.exec(`openssl pkcs12 -export -in ${tmpCrtPath} -inkey ${tmpKeyPath} -out ${p12Path} -name certd ${passwordArg}`);
|
||||
|
||||
const jksPath = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.jks`);
|
||||
const dir = path.dirname(jksPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
await this.exec(`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `);
|
||||
fs.unlinkSync(p12Path);
|
||||
|
||||
const fileBuffer = fs.readFileSync(jksPath);
|
||||
const certBase64 = fileBuffer.toString("base64");
|
||||
fs.unlinkSync(jksPath);
|
||||
return certBase64;
|
||||
} catch (e) {
|
||||
this.logger.error("转换jks失败", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import { IAccess, IServiceGetter, Registrable } from "@certd/pipeline";
|
||||
|
||||
export type DnsProviderDefine = Registrable & {
|
||||
accessType: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export type CreateRecordOptions = {
|
||||
domain: string;
|
||||
fullRecord: string;
|
||||
hostRecord: string;
|
||||
type: string;
|
||||
value: any;
|
||||
};
|
||||
export type RemoveRecordOptions<T> = {
|
||||
recordReq: CreateRecordOptions;
|
||||
// 本次创建的dns解析记录,实际上就是createRecord接口的返回值
|
||||
recordRes: T;
|
||||
};
|
||||
|
||||
export type DnsProviderContext = {
|
||||
access: IAccess;
|
||||
logger: ILogger;
|
||||
http: HttpClient;
|
||||
utils: typeof utils;
|
||||
domainParser: IDomainParser;
|
||||
serviceGetter: IServiceGetter;
|
||||
};
|
||||
|
||||
export interface IDnsProvider<T = any> {
|
||||
onInstance(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 中文转英文
|
||||
* @param domain
|
||||
*/
|
||||
punyCodeEncode(domain: string): string;
|
||||
|
||||
/**
|
||||
* 转中文域名
|
||||
* @param domain
|
||||
*/
|
||||
punyCodeDecode(domain: string): string;
|
||||
|
||||
createRecord(options: CreateRecordOptions): Promise<T>;
|
||||
|
||||
removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
|
||||
|
||||
setCtx(ctx: DnsProviderContext): void;
|
||||
|
||||
//中文域名是否需要punycode转码,如果返回True,则使用punycode来添加解析记录,否则使用中文域名添加解析记录
|
||||
usePunyCode(): boolean;
|
||||
}
|
||||
|
||||
export interface ISubDomainsGetter {
|
||||
getSubDomains(): Promise<string[]>;
|
||||
}
|
||||
|
||||
export interface IDomainParser {
|
||||
parse(fullDomain: string): Promise<string>;
|
||||
}
|
||||
|
||||
export type DnsVerifier = {
|
||||
// dns直接校验
|
||||
dnsProviderType?: string;
|
||||
dnsProviderAccessId?: number;
|
||||
};
|
||||
|
||||
export type CnameVerifier = {
|
||||
hostRecord: string;
|
||||
domain: string;
|
||||
recordValue: string;
|
||||
};
|
||||
|
||||
export type HttpVerifier = {
|
||||
// http校验
|
||||
httpUploaderType: string;
|
||||
httpUploaderAccess: number;
|
||||
httpUploadRootDir: string;
|
||||
};
|
||||
export type DomainVerifier = {
|
||||
domain: string;
|
||||
mainDomain: string;
|
||||
type: string;
|
||||
dns?: DnsVerifier;
|
||||
cname?: CnameVerifier;
|
||||
http?: HttpVerifier;
|
||||
};
|
||||
|
||||
export type DomainVerifiers = {
|
||||
[key: string]: DomainVerifier;
|
||||
};
|
||||
|
||||
export interface IDomainVerifierGetter {
|
||||
getVerifiers(domains: string[]): Promise<DomainVerifiers>;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||
import { dnsProviderRegistry } from "./registry.js";
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
import punycode from "punycode.js";
|
||||
export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
||||
ctx!: DnsProviderContext;
|
||||
http!: HttpClient;
|
||||
logger!: ILogger;
|
||||
|
||||
usePunyCode(): boolean {
|
||||
//是否使用punycode来添加解析记录
|
||||
//默认都使用原始中文域名来添加
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文转英文
|
||||
* @param domain
|
||||
*/
|
||||
punyCodeEncode(domain: string) {
|
||||
return punycode.toASCII(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转中文域名
|
||||
* @param domain
|
||||
*/
|
||||
punyCodeDecode(domain: string) {
|
||||
return punycode.toUnicode(domain);
|
||||
}
|
||||
|
||||
setCtx(ctx: DnsProviderContext) {
|
||||
this.ctx = ctx;
|
||||
this.logger = ctx.logger;
|
||||
this.http = ctx.http;
|
||||
}
|
||||
|
||||
async parseDomain(fullDomain: string) {
|
||||
return await this.ctx.domainParser.parse(fullDomain);
|
||||
}
|
||||
|
||||
abstract createRecord(options: CreateRecordOptions): Promise<T>;
|
||||
|
||||
abstract onInstance(): Promise<void>;
|
||||
|
||||
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
|
||||
}
|
||||
|
||||
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {
|
||||
const { dnsProviderType, context } = opts;
|
||||
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);
|
||||
const DnsProviderClass = await dnsProviderPlugin.target();
|
||||
const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine;
|
||||
if (dnsProviderDefine.deprecated) {
|
||||
context.logger.warn(dnsProviderDefine.deprecated);
|
||||
}
|
||||
// @ts-ignore
|
||||
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
||||
dnsProvider.setCtx(context);
|
||||
await dnsProvider.onInstance();
|
||||
return dnsProvider;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { dnsProviderRegistry } from "./registry.js";
|
||||
import { DnsProviderDefine } from "./api.js";
|
||||
import { Decorator } from "@certd/pipeline";
|
||||
import * as _ from "lodash-es";
|
||||
|
||||
// 提供一个唯一 key
|
||||
export const DNS_PROVIDER_CLASS_KEY = "pipeline:dns-provider";
|
||||
|
||||
export function IsDnsProvider(define: DnsProviderDefine): ClassDecorator {
|
||||
return (target: any) => {
|
||||
if (process.env.certd_plugin_loadmode === "metadata") {
|
||||
return;
|
||||
}
|
||||
target = Decorator.target(target);
|
||||
|
||||
Reflect.defineMetadata(DNS_PROVIDER_CLASS_KEY, define, target);
|
||||
|
||||
target.define = define;
|
||||
dnsProviderRegistry.register(define.name, {
|
||||
define,
|
||||
target: async () => {
|
||||
return target;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { IDomainParser, ISubDomainsGetter } from "./api";
|
||||
//@ts-ignore
|
||||
import psl from "psl";
|
||||
import { ILogger, utils, logger as globalLogger } from "@certd/basic";
|
||||
import { resolveDomainBySoaRecord } from "@certd/acme-client";
|
||||
|
||||
export class DomainParser implements IDomainParser {
|
||||
subDomainsGetter: ISubDomainsGetter;
|
||||
logger: ILogger;
|
||||
constructor(subDomainsGetter: ISubDomainsGetter, logger?: ILogger) {
|
||||
this.subDomainsGetter = subDomainsGetter;
|
||||
this.logger = logger || globalLogger;
|
||||
}
|
||||
|
||||
parseDomainByPsl(fullDomain: string) {
|
||||
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
|
||||
if (parsed.error) {
|
||||
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
|
||||
}
|
||||
return parsed.domain as string;
|
||||
}
|
||||
|
||||
async parse(fullDomain: string) {
|
||||
//如果是ip
|
||||
if (utils.domain.isIp(fullDomain)) {
|
||||
return fullDomain;
|
||||
}
|
||||
|
||||
this.logger.info(`查找主域名:${fullDomain}`);
|
||||
const cacheKey = `domain_parse:${fullDomain}`;
|
||||
const value = utils.cache.get(cacheKey);
|
||||
if (value) {
|
||||
this.logger.info(`从缓存获取到主域名:${fullDomain}->${value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const subDomains = await this.subDomainsGetter.getSubDomains();
|
||||
if (subDomains && subDomains.length > 0) {
|
||||
const fullDomainDot = "." + fullDomain;
|
||||
for (const subDomain of subDomains) {
|
||||
if (fullDomainDot.endsWith("." + subDomain)) {
|
||||
//找到子域名托管
|
||||
utils.cache.set(cacheKey, subDomain, {
|
||||
ttl: 60 * 1000,
|
||||
});
|
||||
this.logger.info(`获取到子域名托管域名:${fullDomain}->${subDomain}`);
|
||||
return subDomain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = this.parseDomainByPsl(fullDomain);
|
||||
this.logger.info(`从psl获取主域名:${fullDomain}->${res}`);
|
||||
|
||||
let soaManDomain = null;
|
||||
try {
|
||||
const mainDomain = await resolveDomainBySoaRecord(fullDomain);
|
||||
if (mainDomain) {
|
||||
this.logger.info(`从SOA获取到主域名:${fullDomain}->${mainDomain}`);
|
||||
soaManDomain = mainDomain;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error("从SOA获取主域名失败", e.message);
|
||||
}
|
||||
if (soaManDomain && soaManDomain !== res) {
|
||||
this.logger.warn(`SOA获取的主域名(${soaManDomain})和psl获取的主域名(${res})不一致,请确认是否有设置子域名托管`);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./api.js";
|
||||
export * from "./registry.js";
|
||||
export * from "./decorator.js";
|
||||
export * from "./base.js";
|
||||
export * from "./domain-parser.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createRegistry } from "@certd/pipeline";
|
||||
|
||||
export const dnsProviderRegistry = createRegistry("dnsProvider");
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./convert.js";
|
||||
export * from "./cert-reader.js";
|
||||
export * from "./consts.js";
|
||||
export * from "./dns-provider/index.js";
|
||||
@@ -1,32 +0,0 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "ctyun",
|
||||
title: "天翼云授权",
|
||||
desc: "",
|
||||
icon: "ant-design:aliyun-outlined",
|
||||
order: 2,
|
||||
})
|
||||
export class CtyunAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "accessKeyId",
|
||||
component: {
|
||||
placeholder: "accessKeyId",
|
||||
},
|
||||
helper: "[前往创建天翼云AccessKey](https://iam.ctyun.cn/myAccessKey)",
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = "";
|
||||
@AccessInput({
|
||||
title: "securityKey",
|
||||
component: {
|
||||
placeholder: "securityKey",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
helper: "",
|
||||
})
|
||||
securityKey = "";
|
||||
}
|
||||
|
||||
new CtyunAccess();
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./access/ctyun-access.js";
|
||||
@@ -1,77 +0,0 @@
|
||||
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: "端口",
|
||||
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();
|
||||
@@ -1,63 +0,0 @@
|
||||
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<any>) {
|
||||
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 {
|
||||
return 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./access.js";
|
||||
export * from "./client.js";
|
||||
@@ -1,11 +1,4 @@
|
||||
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";
|
||||
export * from "./ctyun/index.js";
|
||||
export * from "./oss/index.js";
|
||||
export * from "./s3/index.js";
|
||||
export * from "./lib/index.js";
|
||||
export * from "./service/index.js";
|
||||
export * from "./cert/index.js";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AbstractTaskPlugin, TaskInstanceContext } from "@certd/pipeline";
|
||||
import { isPlus } from "@certd/plus-core";
|
||||
|
||||
export function mustPlus() {
|
||||
if (!isPlus()) {
|
||||
throw new Error("此插件仅供专业版中使用");
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractPlusTaskPlugin extends AbstractTaskPlugin {
|
||||
setCtx(ctx: TaskInstanceContext) {
|
||||
super.setCtx(ctx);
|
||||
mustPlus();
|
||||
}
|
||||
|
||||
abstract execute(): Promise<void>;
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./ocr-api.js";
|
||||
export * from "./check.js";
|
||||
@@ -1,90 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { OssClientContext } from "./api";
|
||||
|
||||
export class OssClientFactory {
|
||||
async getClassByType(type: string) {
|
||||
if (type === "alioss") {
|
||||
const module = await import("./impls/alioss.js");
|
||||
return module.default;
|
||||
} else if (type === "ssh") {
|
||||
const module = await import("./impls/ssh.js");
|
||||
return module.default;
|
||||
} else if (type === "sftp") {
|
||||
const module = await import("./impls/sftp.js");
|
||||
return module.default;
|
||||
} else if (type === "ftp") {
|
||||
const module = await import("./impls/ftp.js");
|
||||
return module.default;
|
||||
} else if (type === "tencentcos") {
|
||||
const module = await import("./impls/tencentcos.js");
|
||||
return module.default;
|
||||
} else if (type === "qiniuoss") {
|
||||
const module = await import("./impls/qiniuoss.js");
|
||||
return module.default;
|
||||
} else if (type === "s3") {
|
||||
const module = await import("./impls/s3.js");
|
||||
return module.default;
|
||||
} else {
|
||||
throw new Error(`暂不支持此文件上传方式: ${type}`);
|
||||
}
|
||||
}
|
||||
async createOssClientByType(type: string, opts: { rootDir?: string; access: any; ctx: OssClientContext }) {
|
||||
const cls = await this.getClassByType(type);
|
||||
if (cls) {
|
||||
// @ts-ignore
|
||||
const instance = new cls(opts);
|
||||
await instance.setCtx(opts.ctx);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ossClientFactory = new OssClientFactory();
|
||||
@@ -1,57 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
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 }));
|
||||
if (!res.Contents) {
|
||||
return [];
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
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],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
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 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) {
|
||||
if (!filePath) {
|
||||
filePath = "";
|
||||
}
|
||||
filePath = filePath.trim();
|
||||
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 key = this.rootDir + filePath;
|
||||
try {
|
||||
const client = new SshClient(this.logger);
|
||||
await client.uploadFiles({
|
||||
connectConf: this.access,
|
||||
mkdirs: true,
|
||||
transports: [
|
||||
{
|
||||
localPath: tmpFilePath,
|
||||
remotePath: key,
|
||||
},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
// Remove temp file
|
||||
fs.unlinkSync(tmpFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(filePath: string, opts?: { joinRootDir?: boolean }) {
|
||||
if (opts?.joinRootDir !== false) {
|
||||
filePath = this.join(this.rootDir, filePath);
|
||||
}
|
||||
const client = new SshClient(this.logger);
|
||||
await client.removeFiles({
|
||||
connectConf: this.access,
|
||||
files: [filePath],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./factory.js";
|
||||
export * from "./api.js";
|
||||
@@ -1,31 +0,0 @@
|
||||
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();
|
||||
@@ -1,26 +0,0 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "qiniu",
|
||||
title: "七牛云授权",
|
||||
desc: "",
|
||||
icon: "svg:icon-qiniuyun",
|
||||
input: {},
|
||||
order: 2,
|
||||
})
|
||||
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();
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./access.js";
|
||||
export * from "./access-oss.js";
|
||||
export * from "./lib/sdk.js";
|
||||
@@ -1,180 +0,0 @@
|
||||
import { HttpClient, ILogger, safePromise, utils } from "@certd/basic";
|
||||
import { QiniuAccess } from "../access.js";
|
||||
import fs from "fs";
|
||||
|
||||
export type QiniuCertInfo = {
|
||||
key: string;
|
||||
crt: string;
|
||||
};
|
||||
export class QiniuClient {
|
||||
http: HttpClient;
|
||||
access: QiniuAccess;
|
||||
logger: ILogger;
|
||||
constructor(opts: { http: HttpClient; access: QiniuAccess; logger: ILogger }) {
|
||||
this.http = opts.http;
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
async uploadCert(cert: QiniuCertInfo, certName?: string) {
|
||||
const url = "https://api.qiniu.com/sslcert";
|
||||
|
||||
const body = {
|
||||
name: certName,
|
||||
common_name: "certd",
|
||||
pri: cert.key,
|
||||
ca: cert.crt,
|
||||
};
|
||||
|
||||
const res = await this.doRequest(url, "post", body);
|
||||
|
||||
return res.certID;
|
||||
}
|
||||
|
||||
async bindCert(body: { certid: string; domain: string }) {
|
||||
const url = "https://api.qiniu.com/cert/bind";
|
||||
return await this.doRequest(url, "post", body);
|
||||
}
|
||||
|
||||
async getCertBindings() {
|
||||
const url = "https://api.qiniu.com/cert/bindings";
|
||||
const res = await this.doRequest(url, "get");
|
||||
return res;
|
||||
}
|
||||
|
||||
async doRequest(url: string, method: string, body?: any) {
|
||||
const { generateAccessToken } = await import("qiniu/qiniu/util.js");
|
||||
const token = generateAccessToken(this.access, url);
|
||||
const res = await this.http.request({
|
||||
url,
|
||||
method: method,
|
||||
headers: {
|
||||
Authorization: token,
|
||||
},
|
||||
data: body,
|
||||
logRes: false,
|
||||
});
|
||||
if (res && res.error) {
|
||||
if (res.error.includes("domaintype")) {
|
||||
throw new Error("请求失败:" + res.error + ",该域名属于CDN域名,请使用部署到七牛云CDN插件");
|
||||
}
|
||||
throw new Error("请求失败:" + res.error);
|
||||
}
|
||||
console.log("res", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async doRequestV2(opts: { url: string; method: string; body?: any; contentType: string }) {
|
||||
const { HttpClient } = await import("qiniu/qiniu/httpc/client.js");
|
||||
const { QiniuAuthMiddleware } = await import("qiniu/qiniu/httpc/middleware/qiniuAuth.js");
|
||||
// X-Qiniu-Date: 20060102T150405Z
|
||||
const auth = new QiniuAuthMiddleware({
|
||||
mac: {
|
||||
...this.access,
|
||||
options: {},
|
||||
},
|
||||
});
|
||||
const http = new HttpClient({ timeout: 10000, middlewares: [auth] });
|
||||
console.log("http", http);
|
||||
|
||||
return safePromise((resolve, reject) => {
|
||||
try {
|
||||
http.get({
|
||||
url: opts.url,
|
||||
headers: {
|
||||
"Content-Type": opts.contentType,
|
||||
},
|
||||
callback: (nullable, res) => {
|
||||
console.log("nullable", nullable, "res", res);
|
||||
if (res?.error) {
|
||||
reject(res);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
const options = {
|
||||
scope: bucket,
|
||||
};
|
||||
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||
const uploadToken = putPolicy.uploadToken(mac);
|
||||
|
||||
const config = new qiniu.conf.Config();
|
||||
const formUploader = new qiniu.form_up.FormUploader(config);
|
||||
const putExtra = new qiniu.form_up.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;
|
||||
} else {
|
||||
console.log(resp.statusCode);
|
||||
throw new Error("上传失败:" + JSON.stringify(resp));
|
||||
}
|
||||
}
|
||||
|
||||
async removeFile(bucket: string, key: string) {
|
||||
const bucketManager = await this.getBucketManager();
|
||||
|
||||
const { resp } = await bucketManager.delete(bucket, key);
|
||||
|
||||
if (resp.statusCode === 200) {
|
||||
this.logger.info("文件删除成功:" + key);
|
||||
return;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { ossClientFactory } from "../oss/index.js";
|
||||
import S3OssClientImpl from "../oss/impls/s3.js";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在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;
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest",
|
||||
},
|
||||
helper: "点击测试接口是否正常",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const client: S3OssClientImpl = await ossClientFactory.createOssClientByType("s3", {
|
||||
access: this,
|
||||
rootDir: "",
|
||||
ctx: {
|
||||
accessService: this.ctx.accessService,
|
||||
logger: this.ctx.logger,
|
||||
utils: this.ctx.utils,
|
||||
},
|
||||
});
|
||||
|
||||
await client.listDir("/");
|
||||
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
new S3Access();
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./access.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./ssh.js";
|
||||
export * from "./ssh-access.js";
|
||||
export * from "./sftp-access.js";
|
||||
@@ -1,34 +0,0 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "sftp",
|
||||
title: "SFTP授权",
|
||||
desc: "",
|
||||
icon: "clarity:host-line",
|
||||
input: {},
|
||||
})
|
||||
export class SftpAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "SSH授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "ssh",
|
||||
vModel: "modelValue",
|
||||
},
|
||||
helper: "请选择一个SSH授权",
|
||||
required: true,
|
||||
})
|
||||
sshAccess!: string;
|
||||
@AccessInput({
|
||||
title: "文件权限",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
placeholder: "777",
|
||||
},
|
||||
helper: "文件上传后是否修改文件权限",
|
||||
})
|
||||
fileMode!: string;
|
||||
}
|
||||
|
||||
new SftpAccess();
|
||||
@@ -1,173 +0,0 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
@IsAccess({
|
||||
name: "ssh",
|
||||
title: "主机登录授权",
|
||||
desc: "",
|
||||
icon: "clarity:host-line",
|
||||
input: {},
|
||||
order: 0,
|
||||
})
|
||||
export class SshAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "主机地址",
|
||||
component: {
|
||||
placeholder: "主机域名或IP地址",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
host!: string;
|
||||
@AccessInput({
|
||||
title: "端口",
|
||||
value: 22,
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
placeholder: "22",
|
||||
},
|
||||
rules: [{ required: true, message: "此项必填" }],
|
||||
})
|
||||
port!: number;
|
||||
@AccessInput({
|
||||
title: "用户名",
|
||||
value: "root",
|
||||
rules: [{ required: true, message: "此项必填" }],
|
||||
})
|
||||
username!: string;
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
},
|
||||
encrypt: true,
|
||||
helper: "登录密码或密钥必填一项",
|
||||
})
|
||||
password!: string;
|
||||
@AccessInput({
|
||||
title: "私钥登录",
|
||||
helper: "私钥或密码必填一项",
|
||||
component: {
|
||||
name: "pem-input",
|
||||
vModel: "modelValue",
|
||||
},
|
||||
encrypt: true,
|
||||
})
|
||||
privateKey!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "私钥密码",
|
||||
helper: "如果你的私钥有密码的话",
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
},
|
||||
encrypt: true,
|
||||
})
|
||||
passphrase!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "脚本类型",
|
||||
helper: "bash 、sh 、fish",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "default", label: "默认" },
|
||||
{ value: "sh", label: "sh" },
|
||||
{ value: "bash", label: "bash" },
|
||||
{ value: "fish", label: "fish(不支持set -e)" },
|
||||
],
|
||||
},
|
||||
})
|
||||
scriptType: 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",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
placeholder: "socks5://user:password@host:port",
|
||||
},
|
||||
encrypt: false,
|
||||
})
|
||||
socksProxy!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "超时时间",
|
||||
helper: "执行命令的超时时间,单位秒,默认30分钟",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
},
|
||||
})
|
||||
timeout: number;
|
||||
|
||||
@AccessInput({
|
||||
title: "是否Windows",
|
||||
helper: "如果是Windows主机,请勾选此项\n并且需要windows[安装OpenSSH](https://certd.docmirror.cn/guide/use/host/windows.html)",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
})
|
||||
windows = false;
|
||||
|
||||
@AccessInput({
|
||||
title: "命令编码",
|
||||
helper: "如果是Windows主机,且出现乱码了,请尝试设置为GBK",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "", label: "默认" },
|
||||
{ value: "GBK", label: "GBK" },
|
||||
{ value: "UTF8", label: "UTF-8" },
|
||||
],
|
||||
},
|
||||
})
|
||||
encoding: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
type: "access",
|
||||
typeName: "ssh",
|
||||
action: "TestRequest",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
`,
|
||||
helper: "点击测试",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const { SshClient } = await import("./ssh.js");
|
||||
const client = new SshClient(this.ctx.logger);
|
||||
|
||||
const script = ["echo hello", "exit"];
|
||||
await client.exec({
|
||||
connectConf: this,
|
||||
script: script,
|
||||
});
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
new SshAccess();
|
||||
@@ -1,643 +0,0 @@
|
||||
// @ts-ignore
|
||||
import path from "path";
|
||||
import { isArray } from "lodash-es";
|
||||
import { ILogger, safePromise } from "@certd/basic";
|
||||
import { SshAccess } from "./ssh-access.js";
|
||||
|
||||
import fs from "fs";
|
||||
import { SocksProxyType } from "socks/typings/common/constants";
|
||||
|
||||
export type TransportItem = { localPath: string; remotePath: string };
|
||||
export interface SocksProxy {
|
||||
ipaddress?: string;
|
||||
host?: string;
|
||||
port: number;
|
||||
type: any;
|
||||
userId?: string;
|
||||
password?: string;
|
||||
custom_auth_method?: number;
|
||||
custom_auth_request_handler?: () => Promise<Buffer>;
|
||||
custom_auth_response_size?: number;
|
||||
custom_auth_response_handler?: (data: Buffer) => Promise<boolean>;
|
||||
}
|
||||
export type SshConnectConfig = {
|
||||
sock?: any;
|
||||
};
|
||||
|
||||
export class AsyncSsh2Client {
|
||||
conn: any;
|
||||
logger: ILogger;
|
||||
connConf: SshAccess & SshConnectConfig;
|
||||
windows = false;
|
||||
encoding: string;
|
||||
constructor(connConf: SshAccess, logger: ILogger) {
|
||||
this.connConf = connConf;
|
||||
this.logger = logger;
|
||||
this.windows = connConf.windows || false;
|
||||
this.encoding = connConf.encoding;
|
||||
}
|
||||
|
||||
convert(iconv: any, buffer: Buffer) {
|
||||
if (this.encoding) {
|
||||
return iconv.decode(buffer, this.encoding);
|
||||
}
|
||||
return buffer.toString().replaceAll("\r\n", "\n");
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this.logger.info(`开始连接,${this.connConf.host}:${this.connConf.port}`);
|
||||
if (this.connConf.socksProxy) {
|
||||
this.logger.info(`使用代理${this.connConf.socksProxy}`);
|
||||
if (typeof this.connConf.port === "string") {
|
||||
this.connConf.port = parseInt(this.connConf.port);
|
||||
}
|
||||
|
||||
const { SocksClient } = await import("socks");
|
||||
|
||||
const proxyOption = this.parseSocksProxyFromUri(this.connConf.socksProxy);
|
||||
const info = await SocksClient.createConnection({
|
||||
proxy: proxyOption,
|
||||
command: "connect",
|
||||
destination: {
|
||||
host: this.connConf.host,
|
||||
port: this.connConf.port,
|
||||
},
|
||||
});
|
||||
this.logger.info("代理连接成功");
|
||||
this.connConf.sock = info.socket;
|
||||
}
|
||||
|
||||
const ssh2 = await import("ssh2");
|
||||
const ssh2Constants = await import("ssh2/lib/protocol/constants.js");
|
||||
const { SUPPORTED_KEX, SUPPORTED_SERVER_HOST_KEY, SUPPORTED_CIPHER, SUPPORTED_MAC } = ssh2Constants.default;
|
||||
return safePromise((resolve, reject) => {
|
||||
try {
|
||||
const conn = new ssh2.default.Client();
|
||||
conn
|
||||
.on("error", (err: any) => {
|
||||
this.logger.error("连接失败", err);
|
||||
reject(err);
|
||||
})
|
||||
.on("ready", () => {
|
||||
this.logger.info("连接成功");
|
||||
this.conn = conn;
|
||||
resolve(this.conn);
|
||||
})
|
||||
.on("keyboard-interactive", (name, descr, lang, prompts, finish) => {
|
||||
// For illustration purposes only! It's not safe to do this!
|
||||
// You can read it from process.stdin or whatever else...
|
||||
const password = this.connConf.password;
|
||||
return finish([password]);
|
||||
|
||||
// And remember, server may trigger this event multiple times
|
||||
// and for different purposes (not only auth)
|
||||
})
|
||||
.connect({
|
||||
...this.connConf,
|
||||
tryKeyboard: true,
|
||||
algorithms: {
|
||||
serverHostKey: SUPPORTED_SERVER_HOST_KEY,
|
||||
cipher: SUPPORTED_CIPHER,
|
||||
hmac: SUPPORTED_MAC,
|
||||
kex: SUPPORTED_KEX,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
async getSftp() {
|
||||
return safePromise((resolve, reject) => {
|
||||
this.logger.info("获取sftp");
|
||||
this.conn.sftp((err: any, sftp: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(sftp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fastPut(options: { sftp: any; localPath: string; remotePath: string; opts?: { mode?: string } }) {
|
||||
const { sftp, localPath, remotePath, opts } = options;
|
||||
return safePromise((resolve, reject) => {
|
||||
this.logger.info(`开始上传:${localPath} => ${remotePath}`);
|
||||
sftp.fastPut(localPath, remotePath, { ...(opts ?? {}) }, (err: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
this.logger.error("请确认路径是否包含文件名,路径本身不能是目录,路径不能有*?之类的特殊符号,要有写入权限");
|
||||
return;
|
||||
}
|
||||
this.logger.info(`上传文件成功:${localPath} => ${remotePath}`);
|
||||
resolve({});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listDir(options: { sftp: any; remotePath: string }) {
|
||||
const { sftp, remotePath } = options;
|
||||
return safePromise((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 safePromise((resolve, reject) => {
|
||||
this.logger.info(`开始删除远程文件:${remotePath}`);
|
||||
sftp.unlink(remotePath, (err: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
this.logger.info(`删除文件成功:${remotePath}`);
|
||||
resolve({});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param script
|
||||
* @param opts {withStdErr 返回{stdOut,stdErr}}
|
||||
*/
|
||||
async exec(
|
||||
script: string,
|
||||
opts: {
|
||||
throwOnStdErr?: boolean;
|
||||
withStdErr?: boolean;
|
||||
env?: any;
|
||||
} = {}
|
||||
): Promise<string> {
|
||||
if (!script) {
|
||||
this.logger.info("script 为空,取消执行");
|
||||
return;
|
||||
}
|
||||
let iconv: any = await import("iconv-lite");
|
||||
iconv = iconv.default;
|
||||
// if (this.connConf.windows) {
|
||||
// script += "\r\nexit\r\n";
|
||||
// //保证windows下正常退出
|
||||
// }
|
||||
|
||||
if (script.includes(" -i ")) {
|
||||
this.logger.warn("不支持交互式命令,请不要使用-i参数");
|
||||
}
|
||||
|
||||
return safePromise((resolve, reject) => {
|
||||
this.logger.info(`执行命令:[${this.connConf.host}][exec]: \n` + script);
|
||||
// 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;
|
||||
}
|
||||
let data = "";
|
||||
let stdErr = "";
|
||||
let hasErrorLog = false;
|
||||
stream
|
||||
.on("close", (code: any, signal: any) => {
|
||||
this.logger.info(`[${this.connConf.host}][close]:code:${code}`);
|
||||
/**
|
||||
* ]pipeline 执行命令:[10.123.0.2][exec]:cd /d D:\nginx-1.27.5 && D:\nginx-1.27.5\nginx.exe -t && D:\nginx-1.27.5\nginx.exe -s reload
|
||||
* [2025-07-09T10:24:11.219] [ERROR]pipeline - [10. 123.0. 2][error]: nginx: the configuration file D: \nginx-1.27. 5/conf/nginx. conf syntax is ok
|
||||
* [2025-07-09T10:24:11.231] [ERROR][10. 123. 0. 2] [error]: nginx: configuration file D: \nginx-1.27.5/conf/nginx.conf test is successful
|
||||
* pipeline-
|
||||
* [2025-07-09T10:24:11.473] [INFO]pipeline -[10.123.0.2][close]:code:0
|
||||
* [2025-07-09T10:24:11.473][ERRoR] pipeline- [step][主机一执行远程主机脚本命令]<id:53hyarN3yvmbijNuMiNAt>: [Eror: nginx: the configuration fileD:\nginx-1.27.5/conf/nginx.conf syntax is ok
|
||||
//需要忽略windows的错误
|
||||
*/
|
||||
// if (opts.throwOnStdErr == null && this.windows) {
|
||||
// opts.throwOnStdErr = true;
|
||||
// }
|
||||
if (opts.throwOnStdErr && hasErrorLog) {
|
||||
reject(new Error(data));
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
if (opts.withStdErr === true) {
|
||||
//@ts-ignore
|
||||
resolve({
|
||||
stdErr,
|
||||
stdOut: data,
|
||||
});
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(data));
|
||||
}
|
||||
})
|
||||
.on("data", (ret: Buffer) => {
|
||||
const out = this.convert(iconv, ret);
|
||||
data += out;
|
||||
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
|
||||
})
|
||||
.on("error", (err: any) => {
|
||||
reject(err);
|
||||
this.logger.error(err);
|
||||
})
|
||||
.stderr.on("data", (ret: Buffer) => {
|
||||
const err = this.convert(iconv, ret);
|
||||
stdErr += err;
|
||||
hasErrorLog = true;
|
||||
if (err.includes("sudo: a password is required")) {
|
||||
this.logger.warn("请配置sudo免密,否则命令无法执行");
|
||||
}
|
||||
this.logger.error(`[${this.connConf.host}][error]: ` + err.trimEnd());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async shell(script: string | string[]): Promise<string> {
|
||||
const stripAnsiModule = await import("strip-ansi");
|
||||
const stripAnsi = stripAnsiModule.default;
|
||||
return safePromise<any>((resolve, reject) => {
|
||||
this.logger.info(`执行shell脚本:[${this.connConf.host}][shell]: ` + script);
|
||||
this.conn.shell((err: Error, stream: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let output = "";
|
||||
function ansiHandle(data: string) {
|
||||
data = data.replace(/\[[0-9]+;1H/g, "");
|
||||
data = stripAnsi(data);
|
||||
return data.replaceAll("\r\n", "\n");
|
||||
}
|
||||
stream
|
||||
.on("close", (code: any) => {
|
||||
this.logger.info("Stream :: close,code: " + code);
|
||||
resolve(output);
|
||||
})
|
||||
.on("data", (ret: Buffer) => {
|
||||
const data = ansiHandle(ret.toString());
|
||||
this.logger.info(data);
|
||||
output += data;
|
||||
})
|
||||
.on("error", (err: any) => {
|
||||
reject(err);
|
||||
this.logger.error(err);
|
||||
})
|
||||
.stderr.on("data", (ret: Buffer) => {
|
||||
const data = ansiHandle(ret.toString());
|
||||
output += data;
|
||||
this.logger.error(`[${this.connConf.host}][error]: ` + data);
|
||||
});
|
||||
//保证windows下正常退出
|
||||
const exit = "\r\nexit\r\n";
|
||||
stream.end(script + exit);
|
||||
});
|
||||
});
|
||||
}
|
||||
end() {
|
||||
if (this.conn) {
|
||||
this.conn.end();
|
||||
this.conn.destroy();
|
||||
this.conn = null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseSocksProxyFromUri(socksProxyUri: string): SocksProxy {
|
||||
const url = new URL(socksProxyUri);
|
||||
let type: SocksProxyType = 5;
|
||||
if (url.protocol.startsWith("socks4")) {
|
||||
type = 4;
|
||||
}
|
||||
const proxy: SocksProxy = {
|
||||
host: url.hostname,
|
||||
port: parseInt(url.port),
|
||||
type,
|
||||
};
|
||||
if (url.username) {
|
||||
proxy.userId = url.username;
|
||||
}
|
||||
if (url.password) {
|
||||
proxy.password = url.password;
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
async download(param: { remotePath: string; savePath: string; sftp: any }) {
|
||||
return safePromise((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 {
|
||||
logger: ILogger;
|
||||
/**
|
||||
*
|
||||
* @param connectConf
|
||||
{
|
||||
host: '192.168.100.100',
|
||||
port: 22,
|
||||
username: 'frylock',
|
||||
password: 'nodejsrules'
|
||||
}
|
||||
* @param options
|
||||
*/
|
||||
async uploadFiles(options: { connectConf: SshAccess; transports: TransportItem[]; mkdirs: boolean; opts?: { mode?: string }; uploadType?: string }) {
|
||||
const { connectConf, transports, mkdirs, opts } = options;
|
||||
await this._call({
|
||||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
this.logger.info("开始上传");
|
||||
if (mkdirs !== false) {
|
||||
this.logger.info("初始化父目录");
|
||||
for (const transport of transports) {
|
||||
const filePath = path.dirname(transport.remotePath);
|
||||
let mkdirCmd = `mkdir -p ${filePath} `;
|
||||
if (conn.windows) {
|
||||
if (filePath.indexOf("/") > -1) {
|
||||
this.logger.info("--------------------------");
|
||||
this.logger.info("请注意:windows下,文件目录分隔应该写成\\而不是/");
|
||||
this.logger.info("--------------------------");
|
||||
}
|
||||
const isCmd = await this.isCmd(conn);
|
||||
if (!isCmd) {
|
||||
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
|
||||
} else {
|
||||
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
|
||||
}
|
||||
}
|
||||
await conn.exec(mkdirCmd);
|
||||
}
|
||||
}
|
||||
|
||||
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("文件全部上传成功");
|
||||
},
|
||||
});
|
||||
}
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
async scpUpload(options: { conn: any; localPath: string; remotePath: string; opts?: { mode?: string } }) {
|
||||
const { conn, localPath, remotePath } = options;
|
||||
return safePromise((resolve, reject) => {
|
||||
// 关键步骤:构造 SCP 命令
|
||||
this.logger.info(`开始上传:${localPath} => ${remotePath}`);
|
||||
conn.conn.exec(
|
||||
`scp -t ${remotePath}`, // -t 表示目标模式
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
try {
|
||||
// 准备 SCP 协议头
|
||||
const fileStats = fs.statSync(localPath);
|
||||
const fileName = path.basename(localPath);
|
||||
|
||||
// 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);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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% ");
|
||||
const ret = spec.toString().trim();
|
||||
if (ret.includes("%COMSPEC%") && !ret.includes("echo %COMSPEC%")) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async getIsCmd(options: { connectConf: SshAccess }) {
|
||||
const { connectConf } = options;
|
||||
return await this._call<boolean>({
|
||||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
return await this.isCmd(conn);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
* Start-Service sshd
|
||||
*
|
||||
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
|
||||
* @param options
|
||||
*/
|
||||
async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any; throwOnStdErr?: boolean; stopOnError?: boolean }): Promise<string> {
|
||||
let { script } = options;
|
||||
const { connectConf, throwOnStdErr } = options;
|
||||
|
||||
// this.logger.info('命令:', script);
|
||||
return await this._call({
|
||||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
let isWinCmd = false;
|
||||
const isLinux = !connectConf.windows;
|
||||
const envScripts = [];
|
||||
if (connectConf.windows) {
|
||||
isWinCmd = await this.isCmd(conn);
|
||||
}
|
||||
|
||||
if (options.env) {
|
||||
for (const key in options.env) {
|
||||
if (isLinux) {
|
||||
envScripts.push(`export ${key}=${options.env[key]}`);
|
||||
} else if (isWinCmd) {
|
||||
//win cmd
|
||||
envScripts.push(`set ${key}=${options.env[key]}`);
|
||||
} else {
|
||||
//powershell
|
||||
envScripts.push(`$env:${key}="${options.env[key]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isWinCmd) {
|
||||
if (typeof script === "string") {
|
||||
script = script.split("\n");
|
||||
}
|
||||
//组合成&&的形式
|
||||
script = envScripts.concat(script);
|
||||
script = script as Array<string>;
|
||||
script = script.join(" && ");
|
||||
} else {
|
||||
const newLine = isLinux ? "\n" : "\r\n";
|
||||
if (isArray(script)) {
|
||||
script = script as Array<string>;
|
||||
script = script.join(newLine);
|
||||
}
|
||||
if (envScripts.length > 0) {
|
||||
script = envScripts.join(newLine) + newLine + script;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLinux) {
|
||||
if (options.connectConf.scriptType == "bash") {
|
||||
script = "#!/usr/bin/env bash \n" + script;
|
||||
} else if (options.connectConf.scriptType == "sh") {
|
||||
script = "#!/bin/sh\n" + script;
|
||||
}
|
||||
|
||||
if (options.connectConf.scriptType != "fish" && options.stopOnError !== false) {
|
||||
script = "set -e\n" + script;
|
||||
}
|
||||
}
|
||||
|
||||
return await conn.exec(script as string, { throwOnStdErr });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string> {
|
||||
let { script } = options;
|
||||
const { connectConf } = options;
|
||||
if (isArray(script)) {
|
||||
script = script as Array<string>;
|
||||
if (connectConf.windows) {
|
||||
script = script.join("\r\n");
|
||||
} else {
|
||||
script = script.join("\n");
|
||||
}
|
||||
} else {
|
||||
if (connectConf.windows) {
|
||||
//@ts-ignore
|
||||
script = script.replaceAll("\n", "\r\n");
|
||||
}
|
||||
}
|
||||
return await this._call({
|
||||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
return await conn.shell(script as string);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async _call<T = any>(options: { connectConf: SshAccess; callable: (conn: AsyncSsh2Client) => Promise<T> }): Promise<T> {
|
||||
const { connectConf, callable } = options;
|
||||
const conn = new AsyncSsh2Client(connectConf, this.logger);
|
||||
try {
|
||||
await conn.connect();
|
||||
} catch (e: any) {
|
||||
if (e.message?.indexOf("All configured authentication methods failed") > -1) {
|
||||
this.logger.error(e);
|
||||
throw new Error("登录失败,请检查用户名/密码/密钥是否正确");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
let timeoutId = null;
|
||||
try {
|
||||
timeoutId = setTimeout(() => {
|
||||
this.logger.info("执行超时,断开连接");
|
||||
conn.end();
|
||||
}, 1000 * (connectConf.timeout || 1800));
|
||||
return await callable(conn);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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 = "";
|
||||
}
|
||||
|
||||
new TencentCosAccess();
|
||||
@@ -1,71 +0,0 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "tencent",
|
||||
title: "腾讯云",
|
||||
icon: "svg:icon-tencentcloud",
|
||||
order: 0,
|
||||
})
|
||||
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 = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "站点类型",
|
||||
value: "cn",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{
|
||||
label: "国内站",
|
||||
value: "cn",
|
||||
},
|
||||
{
|
||||
label: "国际站",
|
||||
value: "intl",
|
||||
},
|
||||
],
|
||||
},
|
||||
encrypt: false,
|
||||
rules: [{ required: true, message: "该项必填" }],
|
||||
})
|
||||
accountType: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "关闭证书过期通知",
|
||||
value: true,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
})
|
||||
closeExpiresNotify: boolean = true;
|
||||
|
||||
isIntl() {
|
||||
return this.accountType === "intl";
|
||||
}
|
||||
|
||||
intlDomain() {
|
||||
return this.isIntl() ? "intl." : "";
|
||||
}
|
||||
|
||||
buildEndpoint(endpoint: string) {
|
||||
return `${this.intlDomain()}${endpoint}`;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./access.js";
|
||||
export * from "./access-cos.js";
|
||||
export * from "./lib/index.js";
|
||||
@@ -1,183 +0,0 @@
|
||||
"use strict";
|
||||
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 = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
||||
return g.next = verb(0), g["throw"] = verb(1), g["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 });
|
||||
exports.TencentCosClient = void 0;
|
||||
var basic_1 = require("@certd/basic");
|
||||
var fs_1 = require("fs");
|
||||
var TencentCosClient = /** @class */ (function () {
|
||||
function TencentCosClient(opts) {
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
this.bucket = opts.bucket;
|
||||
this.region = opts.region;
|
||||
}
|
||||
TencentCosClient.prototype.getCosClient = function () {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var sdk, clientConfig;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, Promise.resolve().then(function () { return require("cos-nodejs-sdk-v5"); })];
|
||||
case 1:
|
||||
sdk = _a.sent();
|
||||
clientConfig = {
|
||||
SecretId: this.access.secretId,
|
||||
SecretKey: this.access.secretKey,
|
||||
};
|
||||
return [2 /*return*/, new sdk.default(clientConfig)];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
TencentCosClient.prototype.uploadFile = function (key, file) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var cos;
|
||||
var _this = this;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.getCosClient()];
|
||||
case 1:
|
||||
cos = _a.sent();
|
||||
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
|
||||
var readableStream = file;
|
||||
if (typeof file === "string") {
|
||||
readableStream = fs_1.default.createReadStream(file);
|
||||
}
|
||||
cos.putObject({
|
||||
Bucket: _this.bucket /* 必须 */,
|
||||
Region: _this.region /* 必须 */,
|
||||
Key: key /* 必须 */,
|
||||
Body: readableStream, // 上传文件对象
|
||||
onProgress: function (progressData) {
|
||||
console.log(JSON.stringify(progressData));
|
||||
},
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
})];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
TencentCosClient.prototype.removeFile = function (key) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var cos;
|
||||
var _this = this;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.getCosClient()];
|
||||
case 1:
|
||||
cos = _a.sent();
|
||||
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
|
||||
cos.deleteObject({
|
||||
Bucket: _this.bucket,
|
||||
Region: _this.region,
|
||||
Key: key,
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
})];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
TencentCosClient.prototype.downloadFile = function (key, savePath) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var cos, writeStream;
|
||||
var _this = this;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.getCosClient()];
|
||||
case 1:
|
||||
cos = _a.sent();
|
||||
writeStream = fs_1.default.createWriteStream(savePath);
|
||||
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
|
||||
cos.getObject({
|
||||
Bucket: _this.bucket,
|
||||
Region: _this.region,
|
||||
Key: key,
|
||||
Output: writeStream,
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
})];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
TencentCosClient.prototype.listDir = function (dirKey) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var cos;
|
||||
var _this = this;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.getCosClient()];
|
||||
case 1:
|
||||
cos = _a.sent();
|
||||
return [2 /*return*/, (0, basic_1.safePromise)(function (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);
|
||||
});
|
||||
})];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return TencentCosClient;
|
||||
}());
|
||||
exports.TencentCosClient = TencentCosClient;
|
||||
@@ -1,117 +0,0 @@
|
||||
import { TencentAccess } from "../access.js";
|
||||
import { ILogger, safePromise } from "@certd/basic";
|
||||
import fs from "fs";
|
||||
|
||||
export class TencentCosClient {
|
||||
access: TencentAccess;
|
||||
logger: ILogger;
|
||||
region: string;
|
||||
bucket: string;
|
||||
|
||||
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string }) {
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
this.bucket = opts.bucket;
|
||||
this.region = opts.region;
|
||||
}
|
||||
|
||||
async getCosClient() {
|
||||
const sdk = await import("cos-nodejs-sdk-v5");
|
||||
const clientConfig = {
|
||||
SecretId: this.access.secretId,
|
||||
SecretKey: this.access.secretKey,
|
||||
};
|
||||
return new sdk.default(clientConfig);
|
||||
}
|
||||
|
||||
async uploadFile(key: string, file: Buffer | string) {
|
||||
const cos = await this.getCosClient();
|
||||
return safePromise((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: readableStream, // 上传文件对象
|
||||
onProgress: function (progressData) {
|
||||
console.log(JSON.stringify(progressData));
|
||||
},
|
||||
},
|
||||
function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async removeFile(key: string) {
|
||||
const cos = await this.getCosClient();
|
||||
return safePromise((resolve, reject) => {
|
||||
cos.deleteObject(
|
||||
{
|
||||
Bucket: this.bucket,
|
||||
Region: this.region,
|
||||
Key: key,
|
||||
},
|
||||
function (err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async downloadFile(key: string, savePath: string) {
|
||||
const cos = await this.getCosClient();
|
||||
const writeStream = fs.createWriteStream(savePath);
|
||||
return safePromise((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 safePromise((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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./ssl-client.js";
|
||||
export * from "./cos-client.js";
|
||||
@@ -1,102 +0,0 @@
|
||||
import { ILogger } from "@certd/basic";
|
||||
import { TencentAccess } from "../access.js";
|
||||
|
||||
export type TencentCertInfo = {
|
||||
key: string;
|
||||
crt: string;
|
||||
};
|
||||
export class TencentSslClient {
|
||||
access: TencentAccess;
|
||||
logger: ILogger;
|
||||
region?: string;
|
||||
constructor(opts: { access: TencentAccess; logger: ILogger; region?: string }) {
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
this.region = opts.region;
|
||||
}
|
||||
async getSslClient(): Promise<any> {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
const SslClient = sdk.v20191205.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: this.access.secretId,
|
||||
secretKey: this.access.secretKey,
|
||||
},
|
||||
region: this.region,
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: this.access.isIntl() ? "ssl.intl.tencentcloudapi.com" : "ssl.tencentcloudapi.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new SslClient(clientConfig);
|
||||
}
|
||||
|
||||
checkRet(ret: any) {
|
||||
if (!ret || ret.Error) {
|
||||
throw new Error("请求失败:" + ret.Error.Code + "," + ret.Error.Message + ",requestId" + ret.RequestId);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadToTencent(opts: { certName: string; cert: TencentCertInfo }): Promise<string> {
|
||||
const client = await this.getSslClient();
|
||||
const params = {
|
||||
CertificatePublicKey: opts.cert.crt,
|
||||
CertificatePrivateKey: opts.cert.key,
|
||||
Alias: opts.certName,
|
||||
};
|
||||
const ret = await client.UploadCertificate(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info(`证书[${opts.certName}]上传成功:tencentCertId=`, ret.CertificateId);
|
||||
if (this.access.closeExpiresNotify) {
|
||||
await this.switchCertNotify([ret.CertificateId], true);
|
||||
}
|
||||
|
||||
return ret.CertificateId;
|
||||
}
|
||||
|
||||
async switchCertNotify(certIds: string[], disabled: boolean) {
|
||||
const client = await this.getSslClient();
|
||||
const params = {
|
||||
CertificateIds: certIds,
|
||||
SwitchStatus: disabled ? 1 : 0, //1是忽略通知,0是不忽略
|
||||
};
|
||||
const ret = await client.ModifyCertificatesExpiringNotificationSwitch(params);
|
||||
this.checkRet(ret);
|
||||
this.logger.info(`关闭证书${certIds}过期通知成功`);
|
||||
return ret.RequestId;
|
||||
}
|
||||
|
||||
async deployCertificateInstance(params: any) {
|
||||
return await this.doRequest("DeployCertificateInstance", params);
|
||||
}
|
||||
|
||||
async DescribeHostUploadUpdateRecordDetail(params: any) {
|
||||
return await this.doRequest("DescribeHostUploadUpdateRecordDetail", params);
|
||||
}
|
||||
|
||||
async UploadUpdateCertificateInstance(params: any) {
|
||||
return await this.doRequest("UploadUpdateCertificateInstance", params);
|
||||
}
|
||||
|
||||
async DescribeCertificates(params: { Limit?: number; Offset?: number; SearchKey?: string }) {
|
||||
return await this.doRequest("DescribeCertificates", {
|
||||
ExpirationSort: "ASC",
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
async doRequest(action: string, params: any) {
|
||||
const client = await this.getSslClient();
|
||||
try {
|
||||
const res = await client.request(action, params);
|
||||
this.checkRet(res);
|
||||
return res;
|
||||
} catch (e) {
|
||||
this.logger.error(`action ${action} error: ${e.message},requestId=${e.RequestId}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user