feat: 支持lego,海量DNS提供商

pull/101/head
xiaojunnuo 2024-07-18 21:10:13 +08:00
parent b1cd055342
commit 0bc6d0a211
16 changed files with 569 additions and 367 deletions

View File

@ -15,12 +15,14 @@
"dependencies": {
"@types/lodash-es": "^4.17.12",
"axios": "^1.7.2",
"fix-path": "^4.0.0",
"lodash-es": "^4.17.21",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.3",
"qs": "^6.11.2"
},
"devDependencies": {
"iconv-lite": "^0.6.3",
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
@ -33,7 +35,6 @@
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"chai": "4.3.10",
"mocha": "^10.2.0",
"dayjs": "^1.11.7",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
@ -41,6 +42,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"log4js": "^6.9.1",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"reflect-metadata": "^0.1.13",
"rollup": "^3.7.4",

View File

@ -1,43 +0,0 @@
const resolve = require("@rollup/plugin-node-resolve");
const commonjs = require("@rollup/plugin-commonjs");
//const Typescript = require("rollup-plugin-typescript2");
const Typescript = require("@rollup/plugin-typescript");
const json = require("@rollup/plugin-json");
const terser = require("@rollup/plugin-terser");
module.exports = {
input: "src/index.ts",
output: {
file: "dist/bundle.js",
format: "cjs",
},
plugins: [
// 解析第三方依赖
resolve(),
// 识别 commonjs 模式第三方依赖
commonjs(),
Typescript({
target: "esnext",
rootDir: "src",
declaration: true,
declarationDir: "dist/d",
exclude: ["./node_modules/**", "./src/**/*.vue"],
allowSyntheticDefaultImports: true,
}),
json(),
terser(),
],
external: [
"vue",
"lodash-es",
"dayjs",
"@certd/acme-client",
"@certd/pipeline",
"@certd/plugin-cert",
"@certd/plugin-aliyun",
"@certd/plugin-tencent",
"@certd/plugin-huawei",
"@certd/plugin-host",
"@certd/plugin-tencent",
"@certd/plugin-util",
],
};

View File

@ -11,7 +11,11 @@ function attachProperty(target: any, propertyKey: string | symbol) {
}
function getClassProperties(target: any) {
return propertyMap[target] || {};
//获取父类
const parent = Object.getPrototypeOf(target);
const parentMap = propertyMap[parent] || {};
const current = propertyMap[target] || {};
return _.merge({}, parentMap, current);
}
function target(target: any, propertyKey?: string | symbol) {

View File

@ -31,7 +31,24 @@ export function IsTaskPlugin(define: PluginDefine): ClassDecorator {
outputs[property] = output;
}
}
_.merge(define, { input: inputs, autowire: autowires, output: outputs });
// inputs 转换为array根据order排序然后再转换为map
let inputArray = [];
for (const key in inputs) {
const _input = inputs[key];
if (_input.order == null) {
_input.order = 0;
}
inputArray.push([key, _input]);
}
inputArray = _.sortBy(inputArray, (item) => item[1].order);
const inputMap: any = {};
inputArray.forEach((item) => {
inputMap[item[0]] = item[1];
});
_.merge(define, { input: inputMap, autowire: autowires, output: outputs });
Reflect.defineMetadata(PLUGIN_CLASS_KEY, define, target);

View File

@ -2,6 +2,8 @@ import sleep from "./util.sleep.js";
import { request } from "./util.request.js";
export * from "./util.log.js";
export * from "./util.file.js";
export * from "./util.sp.js";
export * as promises from "./util.promise.js";
export const utils = {
sleep,
http: request,

View File

@ -1,3 +1,5 @@
import { logger } from "./util.log.js";
export function TimeoutPromise(callback: () => Promise<void>, ms = 30 * 1000) {
let timeout: any;
return Promise.race([
@ -11,3 +13,14 @@ export function TimeoutPromise(callback: () => Promise<void>, ms = 30 * 1000) {
clearTimeout(timeout);
});
}
export function safePromise<T>(callback: (resolve: (ret: T) => void, reject: (ret: any) => void) => void): Promise<T> {
return new Promise((resolve, reject) => {
try {
callback(resolve, reject);
} catch (e) {
logger.error(e);
reject(e);
}
});
}

View File

@ -5,3 +5,4 @@ export default function (timeout: number) {
}, timeout);
});
}

View File

@ -0,0 +1,110 @@
//转换为import
import childProcess from "child_process";
import { safePromise } from "./util.promise.js";
import { ILogger, logger } from "./util.log.js";
export type ExecOption = {
cmd: string | string[];
env: any;
logger?: ILogger;
options?: any;
};
async function exec(opts: ExecOption): Promise<string> {
let cmd = "";
const log = opts.logger || logger;
if (opts.cmd instanceof Array) {
for (const item of opts.cmd) {
if (cmd) {
cmd += " && " + item;
} else {
cmd = item;
}
}
}
log.info(`执行命令: ${cmd}`);
return safePromise((resolve, reject) => {
childProcess.exec(
cmd,
{
env: {
...process.env,
...opts.env,
},
...opts.options,
},
(error, stdout, stderr) => {
if (error) {
log.error(`exec error: ${error}`);
reject(error);
} else {
const res = stdout.toString("utf-8");
log.info(`stdout: ${res}`);
resolve(res);
}
}
);
});
}
export type SpawnOption = {
cmd: string | string[];
onStdout?: (data: string) => void;
onStderr?: (data: string) => void;
env: any;
logger?: ILogger;
options?: any;
};
async function spawn(opts: SpawnOption): Promise<string> {
let cmd = "";
const log = opts.logger || logger;
if (opts.cmd instanceof Array) {
for (const item of opts.cmd) {
if (cmd) {
cmd += " && " + item;
} else {
cmd = item;
}
}
}
log.info(`执行命令: ${cmd}`);
let stdout = "";
let stderr = "";
return safePromise((resolve, reject) => {
const ls = childProcess.spawn(cmd, {
shell: process.platform == "win32",
env: {
...process.env,
...opts.env,
},
...opts.options,
});
ls.stdout.on("data", (data) => {
log.info(`stdout: ${data}`);
stdout += data;
});
ls.stderr.on("data", (data) => {
log.error(`stderr: ${data}`);
stderr += data;
});
ls.on("error", (error) => {
log.error(`child process error: ${error}`);
reject(error);
});
ls.on("close", (code: number) => {
if (code !== 0) {
log.error(`child process exited with code ${code}`);
reject(new Error(stderr));
} else {
resolve(stdout);
}
});
});
}
export const sp = {
spawn,
exec,
};

View File

@ -1,59 +0,0 @@
import { defineConfig } from "vite";
import visualizer from "rollup-plugin-visualizer";
import typescript from "@rollup/plugin-typescript";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
target: "es2015",
lib: {
entry: "src/index.ts",
name: "CertdPipeline",
},
rollupOptions: {
plugins: [
visualizer(),
typescript({
target: "es2015",
rootDir: "src",
declaration: true,
declarationDir: "dist/d",
exclude: ["./node_modules/**", "./src/**/*.vue"],
allowSyntheticDefaultImports: true,
}),
],
external: [
"vue",
"lodash-es",
"dayjs",
"@certd/acme-client",
"@certd/plugin-cert",
"@certd/plugin-aliyun",
"@certd/plugin-tencent",
"@certd/plugin-huawei",
"@certd/plugin-host",
"@certd/plugin-tencent",
"@certd/plugin-util",
"log4js",
"@midwayjs/core",
"@midwayjs/decorator",
],
output: {
globals: {
vue: "Vue",
lodash: "_",
dayjs: "dayjs",
"@certd/plugin-cert": "CertdPluginCert",
"@certd/acme-client": "CertdAcmeClient",
"@certd/plugin-aliyun": "CertdPluginAliyun",
"@certd/plugin-host": "CertdPluginHost",
"@certd/plugin-huawei": "CertdPluginHuawei",
"@certd/plugin-util": "CertdPluginUtil",
log4js: "log4js",
"@midwayjs/core": "MidwayjsCore",
"@midwayjs/decorator": "MidwayjsDecorator",
},
},
},
},
});

View File

@ -0,0 +1,255 @@
import { AbstractTaskPlugin, HttpClient, IAccessService, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import dayjs from "dayjs";
import type { CertInfo } from "./acme.js";
import { Logger } from "log4js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
export { CertReader };
export type { CertInfo };
export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
@TaskInput({
title: "域名",
component: {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
},
required: true,
col: {
span: 24,
},
helper:
"1、支持通配符域名例如 *.foo.com、foo.com、*.test.handsfree.work\n" +
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
"3、多级子域名要分成多个域名输入*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
"4、输入一个回车之后再输入下一个",
})
domains!: string[];
@TaskInput({
title: "邮箱",
component: {
name: "a-input",
vModel: "value",
},
required: true,
helper: "请输入邮箱",
})
email!: string;
@TaskInput({
title: "更新天数",
component: {
name: "a-input-number",
vModel: "value",
},
required: true,
order: 100,
helper: "到期前多少天后更新证书,注意:流水线默认不会自动运行,请设置定时器,每天定时运行本流水线",
})
renewDays!: number;
@TaskInput({
title: "强制更新",
component: {
name: "a-switch",
vModel: "checked",
},
order: 100,
helper: "是否强制重新申请证书",
})
forceUpdate!: string;
@TaskInput({
title: "成功后邮件通知",
value: true,
component: {
name: "a-switch",
vModel: "checked",
},
order: 100,
helper: "申请成功后是否发送邮件通知",
})
successNotify = true;
@TaskInput({
title: "配置说明",
order: 9999,
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
})
intro!: string;
// @TaskInput({
// title: "CsrInfo",
// helper: "暂时没有用",
// })
csrInfo!: string;
logger!: Logger;
userContext!: IContext;
accessService!: IAccessService;
http!: HttpClient;
lastStatus!: Step;
@TaskOutput({
title: "域名证书",
})
cert?: CertInfo;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
this.userContext = this.ctx.userContext;
this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step;
await this.onInit();
}
abstract onInit(): Promise<void>;
abstract doCertApply(): Promise<any>;
async execute(): Promise<void> {
const oldCert = await this.condition();
if (oldCert != null) {
return await this.output(oldCert, false);
}
const cert = await this.doCertApply();
if (cert != null) {
await this.output(cert, true);
//清空后续任务的状态,让后续任务能够重新执行
this.clearLastStatus();
if (this.successNotify) {
await this.sendSuccessEmail();
}
} else {
throw new Error("申请证书失败");
}
}
async output(certReader: CertReader, isNew: boolean) {
const cert: CertInfo = certReader.toCertInfo();
this.cert = cert;
if (isNew) {
const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss");
await this.zipCert(cert, applyTime);
} else {
this.extendsFiles();
}
// thi
// s.logger.info(JSON.stringify(certReader.detail));
}
async zipCert(cert: CertInfo, applyTime: string) {
const zip = new JSZip();
zip.file("cert.crt", cert.crt);
zip.file("cert.key", cert.key);
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
const filename = `cert_${domain_name}_${applyTime}.zip`;
const content = await zip.generateAsync({ type: "nodebuffer" });
this.saveFile(filename, content);
this.logger.info(`已保存文件:${filename}`);
}
/**
*
*/
async condition() {
if (this.forceUpdate) {
return null;
}
let inputChanged = false;
const oldInput = JSON.stringify(this.lastStatus?.input?.domains);
const thisInput = JSON.stringify(this.domains);
if (oldInput !== thisInput) {
inputChanged = true;
}
let oldCert: CertReader | undefined = undefined;
try {
oldCert = await this.readLastCert();
} catch (e) {
this.logger.warn("读取cert失败", e);
}
if (oldCert == null) {
this.logger.info("还未申请过,准备申请新证书");
return null;
}
if (inputChanged) {
this.logger.info("输入参数变更,申请新证书");
return null;
}
const ret = this.isWillExpire(oldCert.expires, this.renewDays);
if (!ret.isWillExpire) {
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}`);
return oldCert;
}
this.logger.info("即将过期,开始更新证书");
return null;
}
formatCert(pem: string) {
pem = pem.replace(/\r/g, "");
pem = pem.replace(/\n\n/g, "\n");
pem = pem.replace(/\n$/g, "");
return pem;
}
formatCerts(cert: { crt: string; key: string; csr: string }) {
const newCert: CertInfo = {
crt: this.formatCert(cert.crt),
key: this.formatCert(cert.key),
csr: this.formatCert(cert.csr),
};
return newCert;
}
async readLastCert(): Promise<CertReader | undefined> {
const cert = this.lastStatus?.status?.output?.cert;
if (cert == null) {
return undefined;
}
return new CertReader(cert);
}
/**
* 20
* @param expires
* @param maxDays
* @returns {boolean}
*/
isWillExpire(expires: number, maxDays = 20) {
if (expires == null) {
throw new Error("过期时间不能为空");
}
// 检查有效期
const leftDays = dayjs(expires).diff(dayjs(), "day");
return {
isWillExpire: leftDays < maxDays,
leftDays,
};
}
private async sendSuccessEmail() {
try {
this.logger.info("发送成功邮件通知:" + this.email);
const subject = `【CertD】证书申请成功【${this.domains[0]}`;
await this.ctx.emailService.send({
userId: this.ctx.pipeline.userId,
receivers: [this.email],
subject: subject,
content: `证书申请成功,域名:${this.domains.join(",")}`,
});
} catch (e) {
this.logger.error("send email error", e);
}
}
}

View File

@ -1,12 +1,10 @@
import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import dayjs from "dayjs";
import { AcmeService } from "./acme.js";
import { Decorator, IsTaskPlugin, RunStrategy, TaskInput } from "@certd/pipeline";
import type { CertInfo, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import _ from "lodash-es";
import { Logger } from "log4js";
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
import { CertApplyBasePlugin } from "./base.js";
export { CertReader };
export type { CertInfo };
@ -25,38 +23,7 @@ export type { CertInfo };
},
},
})
export class CertApplyPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "域名",
component: {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
},
required: true,
col: {
span: 24,
},
helper:
"1、支持通配符域名例如 *.foo.com、foo.com、*.test.handsfree.work\n" +
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
"3、多级子域名要分成多个域名输入*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
"4、输入一个回车之后再输入下一个",
})
domains!: string[];
@TaskInput({
title: "邮箱",
component: {
name: "a-input",
vModel: "value",
},
required: true,
helper: "请输入邮箱",
})
email!: string;
export class CertApplyPlugin extends CertApplyBasePlugin {
@TaskInput({
title: "证书提供商",
default: "letsencrypt",
@ -121,69 +88,9 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
})
skipLocalVerify = false;
@TaskInput({
title: "更新天数",
component: {
name: "a-input-number",
vModel: "value",
},
required: true,
helper: "到期前多少天后更新证书,注意:流水线默认不会自动运行,请设置定时器,每天定时运行本流水线",
})
renewDays!: number;
@TaskInput({
title: "强制更新",
component: {
name: "a-switch",
vModel: "checked",
},
helper: "是否强制重新申请证书",
})
forceUpdate!: string;
@TaskInput({
title: "成功后邮件通知",
value: true,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "申请成功后是否发送邮件通知",
})
successNotify = true;
// @TaskInput({
// title: "CsrInfo",
// helper: "暂时没有用",
// })
csrInfo!: string;
@TaskInput({
title: "配置说明",
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
})
intro!: string;
acme!: AcmeService;
logger!: Logger;
userContext!: IContext;
accessService!: IAccessService;
http!: HttpClient;
lastStatus!: Step;
@TaskOutput({
title: "域名证书",
})
cert?: CertInfo;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
this.userContext = this.ctx.userContext;
this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step;
async onInit() {
let eab: any = null;
if (this.eabAccessId) {
eab = await this.ctx.accessService.getById(this.eabAccessId);
@ -197,90 +104,6 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
});
}
async execute(): Promise<void> {
const oldCert = await this.condition();
if (oldCert != null) {
return await this.output(oldCert, false);
}
const cert = await this.doCertApply();
if (cert != null) {
await this.output(cert, true);
//清空后续任务的状态,让后续任务能够重新执行
this.clearLastStatus();
if (this.successNotify) {
await this.sendSuccessEmail();
}
} else {
throw new Error("申请证书失败");
}
}
async output(certReader: CertReader, isNew: boolean) {
const cert: CertInfo = certReader.toCertInfo();
this.cert = cert;
if (isNew) {
const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss");
await this.zipCert(cert, applyTime);
} else {
this.extendsFiles();
}
// thi
// s.logger.info(JSON.stringify(certReader.detail));
}
async zipCert(cert: CertInfo, applyTime: string) {
const zip = new JSZip();
zip.file("cert.crt", cert.crt);
zip.file("cert.key", cert.key);
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
const filename = `cert_${domain_name}_${applyTime}.zip`;
const content = await zip.generateAsync({ type: "nodebuffer" });
this.saveFile(filename, content);
this.logger.info(`已保存文件:${filename}`);
}
/**
*
*/
async condition() {
if (this.forceUpdate) {
return null;
}
let inputChanged = false;
const oldInput = JSON.stringify(this.lastStatus?.input?.domains);
const thisInput = JSON.stringify(this.domains);
if (oldInput !== thisInput) {
inputChanged = true;
}
let oldCert: CertReader | undefined = undefined;
try {
oldCert = await this.readLastCert();
} catch (e) {
this.logger.warn("读取cert失败", e);
}
if (oldCert == null) {
this.logger.info("还未申请过,准备申请新证书");
return null;
}
if (inputChanged) {
this.logger.info("输入参数变更,申请新证书");
return null;
}
const ret = this.isWillExpire(oldCert.expires, this.renewDays);
if (!ret.isWillExpire) {
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}`);
return oldCert;
}
this.logger.info("即将过期,开始更新证书");
return null;
}
async doCertApply() {
const email = this["email"];
const domains = this["domains"];
@ -331,63 +154,6 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
throw e;
}
}
formatCert(pem: string) {
pem = pem.replace(/\r/g, "");
pem = pem.replace(/\n\n/g, "\n");
pem = pem.replace(/\n$/g, "");
return pem;
}
formatCerts(cert: { crt: string; key: string; csr: string }) {
const newCert: CertInfo = {
crt: this.formatCert(cert.crt),
key: this.formatCert(cert.key),
csr: this.formatCert(cert.csr),
};
return newCert;
}
async readLastCert(): Promise<CertReader | undefined> {
const cert = this.lastStatus?.status?.output?.cert;
if (cert == null) {
return undefined;
}
return new CertReader(cert);
}
/**
* 20
* @param expires
* @param maxDays
* @returns {boolean}
*/
isWillExpire(expires: number, maxDays = 20) {
if (expires == null) {
throw new Error("过期时间不能为空");
}
// 检查有效期
const leftDays = dayjs(expires).diff(dayjs(), "day");
return {
isWillExpire: leftDays < maxDays,
leftDays,
};
}
private async sendSuccessEmail() {
try {
this.logger.info("发送成功邮件通知:" + this.email);
const subject = `【CertD】证书申请成功【${this.domains[0]}`;
await this.ctx.emailService.send({
userId: this.ctx.pipeline.userId,
receivers: [this.email],
subject: subject,
content: `证书申请成功,域名:${this.domains.join(",")}`,
});
} catch (e) {
this.logger.error("send email error", e);
}
}
}
new CertApplyPlugin();

View File

@ -0,0 +1,137 @@
import { IsTaskPlugin, RunStrategy, sp, Step, TaskInput } from "@certd/pipeline";
import type { CertInfo } from "./acme.js";
import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
import fs from "fs";
import { EabAccess } from "../../access";
import path from "path";
export { CertReader };
export type { CertInfo };
@IsTaskPlugin({
name: "CertApplyLego",
title: "证书申请Lego",
desc: "支持海量DNS解析提供商推荐使用",
default: {
input: {
renewDays: 20,
forceUpdate: false,
},
strategy: {
runStrategy: RunStrategy.AlwaysRun,
},
},
})
export class CertApplyLegoPlugin extends CertApplyBasePlugin {
@TaskInput({
title: "DNS类型",
component: {
name: "a-input",
vModel: "value",
},
required: true,
})
dnsType!: string;
@TaskInput({
title: "环境变量",
component: {
name: "a-textarea",
vModel: "value",
rows: 6,
},
required: true,
helper: "一行一条,例如 appKeyId=xxxxx",
})
environment!: string;
@TaskInput({
title: "EAB授权",
component: {
name: "pi-access-selector",
type: "eab",
},
helper: "如果需要提供EAB授权",
})
eabAccessId!: number;
@TaskInput({
title: "自定义LEGO参数",
component: {
name: "a-input",
vModel: "value",
},
})
customArgs = "";
eab?: EabAccess;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
this.userContext = this.ctx.userContext;
this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step;
if (this.eabAccessId) {
this.eab = await this.ctx.accessService.getById(this.eabAccessId);
}
}
async onInit(): Promise<void> {}
async doCertApply() {
const env: any = {};
const env_lines = this.environment.split("\n");
for (const line of env_lines) {
const [key, value] = line.trim().split("=");
env[key] = value.trim();
}
let domainArgs = "";
for (const domain of this.domains) {
domainArgs += ` -d "${domain}"`;
}
this.logger.info(`环境变量:${JSON.stringify(env)}`);
let eabArgs = "";
if (this.eab) {
eabArgs = ` --eab "${this.eab.kid}" --kid "${this.eab.kid}" --hmac "${this.eab.hmacKey}"`;
}
const keyType = "-k rsa2048";
const saveDir = `./data/.lego/pipeline_${this.pipeline.id}/`;
const savePathArgs = `--path "${saveDir}"`;
const os_type = process.platform === "win32" ? "windows" : "linux";
const legoPath = path.resolve("./tools", os_type, "lego");
const cmds = [
`${legoPath} -a --email "${this.email}" --dns ${this.dnsType} ${keyType} ${domainArgs} ${eabArgs} ${savePathArgs} ${this.customArgs || ""} run`,
];
await sp.spawn({
cmd: cmds,
logger: this.logger,
env,
});
//读取证书文件
// example.com.crt
// example.com.issuer.crt
// example.com.json
// example.com.key
let domain1 = this.domains[0];
domain1 = domain1.replaceAll("*", "_");
const crtPath = path.resolve(saveDir, "certificates", `${domain1}.crt`);
if (fs.existsSync(crtPath) === false) {
throw new Error(`证书文件不存在,证书申请失败:${crtPath}`);
}
const crt = fs.readFileSync(crtPath, "utf8");
const keyPath = path.resolve(saveDir, "certificates", `${domain1}.key`);
const key = fs.readFileSync(keyPath, "utf8");
const csr = "";
const cert = { crt, key, csr };
const certInfo = this.formatCerts(cert);
return new CertReader(certInfo);
}
}
new CertApplyLegoPlugin();

View File

@ -1 +1,2 @@
export * from "./cert-plugin/index.js";
export * from "./cert-plugin/lego.js";

View File

@ -28,18 +28,22 @@ export class AsyncSsh2Client {
async connect() {
this.logger.info(`开始连接,${this.connConf.host}:${this.connConf.port}`);
return new Promise((resolve, reject) => {
const conn = new ssh2.Client();
conn
.on('error', (err: any) => {
this.logger.error('连接失败', err);
reject(err);
})
.on('ready', () => {
this.logger.info('连接成功');
this.conn = conn;
resolve(this.conn);
})
.connect(this.connConf);
try {
const conn = new ssh2.Client();
conn
.on('error', (err: any) => {
this.logger.error('连接失败', err);
reject(err);
})
.on('ready', () => {
this.logger.info('连接成功');
this.conn = conn;
resolve(this.conn);
})
.connect(this.connConf);
} catch (e) {
reject(e);
}
});
}
async getSftp() {
@ -96,9 +100,7 @@ export class AsyncSsh2Client {
.stderr.on('data', (ret: Buffer) => {
const err = this.convert(ret);
data += err;
this.logger.info(
`[${this.connConf.host}][error]: ` + err.trimEnd()
);
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
});
});
});
@ -154,11 +156,7 @@ export class SshClient {
}
* @param options
*/
async uploadFiles(options: {
connectConf: SshAccess;
transports: any;
mkdirs: boolean;
}) {
async uploadFiles(options: { connectConf: SshAccess; transports: any; mkdirs: boolean }) {
const { connectConf, transports, mkdirs } = options;
await this._call({
connectConf,
@ -172,9 +170,7 @@ export class SshClient {
if (conn.windows) {
if (filePath.indexOf('/') > -1) {
this.logger.info('--------------------------');
this.logger.info(
'请注意windows下文件目录分隔应该写成\\而不是/'
);
this.logger.info('请注意windows下文件目录分隔应该写成\\而不是/');
this.logger.info('--------------------------');
}
const spec = await conn.exec('echo %COMSPEC%');

Binary file not shown.

Binary file not shown.