mirror of https://github.com/certd/certd
feat: 支持lego,海量DNS提供商
parent
b1cd055342
commit
0bc6d0a211
|
@ -15,12 +15,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"fix-path": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"qs": "^6.11.2"
|
"qs": "^6.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
"@rollup/plugin-commonjs": "^23.0.4",
|
"@rollup/plugin-commonjs": "^23.0.4",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
|
@ -33,7 +35,6 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
||||||
"@typescript-eslint/parser": "^5.59.7",
|
"@typescript-eslint/parser": "^5.59.7",
|
||||||
"chai": "4.3.10",
|
"chai": "4.3.10",
|
||||||
"mocha": "^10.2.0",
|
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rollup": "^3.7.4",
|
"rollup": "^3.7.4",
|
||||||
|
|
|
@ -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",
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -11,7 +11,11 @@ function attachProperty(target: any, propertyKey: string | symbol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClassProperties(target: any) {
|
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) {
|
function target(target: any, propertyKey?: string | symbol) {
|
||||||
|
|
|
@ -31,7 +31,24 @@ export function IsTaskPlugin(define: PluginDefine): ClassDecorator {
|
||||||
outputs[property] = output;
|
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);
|
Reflect.defineMetadata(PLUGIN_CLASS_KEY, define, target);
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import sleep from "./util.sleep.js";
|
||||||
import { request } from "./util.request.js";
|
import { request } from "./util.request.js";
|
||||||
export * from "./util.log.js";
|
export * from "./util.log.js";
|
||||||
export * from "./util.file.js";
|
export * from "./util.file.js";
|
||||||
|
export * from "./util.sp.js";
|
||||||
|
export * as promises from "./util.promise.js";
|
||||||
export const utils = {
|
export const utils = {
|
||||||
sleep,
|
sleep,
|
||||||
http: request,
|
http: request,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { logger } from "./util.log.js";
|
||||||
|
|
||||||
export function TimeoutPromise(callback: () => Promise<void>, ms = 30 * 1000) {
|
export function TimeoutPromise(callback: () => Promise<void>, ms = 30 * 1000) {
|
||||||
let timeout: any;
|
let timeout: any;
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
|
@ -11,3 +13,14 @@ export function TimeoutPromise(callback: () => Promise<void>, ms = 30 * 1000) {
|
||||||
clearTimeout(timeout);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -5,3 +5,4 @@ export default function (timeout: number) {
|
||||||
}, timeout);
|
}, timeout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
|
@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
|
import { Decorator, IsTaskPlugin, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { AcmeService } from "./acme.js";
|
|
||||||
import type { CertInfo, SSLProvider } from "./acme.js";
|
import type { CertInfo, SSLProvider } from "./acme.js";
|
||||||
|
import { AcmeService } from "./acme.js";
|
||||||
import _ from "lodash-es";
|
import _ from "lodash-es";
|
||||||
import { Logger } from "log4js";
|
|
||||||
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
|
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
|
||||||
import { CertReader } from "./cert-reader.js";
|
import { CertReader } from "./cert-reader.js";
|
||||||
import JSZip from "jszip";
|
import { CertApplyBasePlugin } from "./base.js";
|
||||||
|
|
||||||
export { CertReader };
|
export { CertReader };
|
||||||
export type { CertInfo };
|
export type { CertInfo };
|
||||||
|
@ -25,38 +23,7 @@ export type { CertInfo };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class CertApplyPlugin extends AbstractTaskPlugin {
|
export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||||
@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({
|
@TaskInput({
|
||||||
title: "证书提供商",
|
title: "证书提供商",
|
||||||
default: "letsencrypt",
|
default: "letsencrypt",
|
||||||
|
@ -121,69 +88,9 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
|
||||||
})
|
})
|
||||||
skipLocalVerify = false;
|
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;
|
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;
|
let eab: any = null;
|
||||||
if (this.eabAccessId) {
|
if (this.eabAccessId) {
|
||||||
eab = await this.ctx.accessService.getById(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() {
|
async doCertApply() {
|
||||||
const email = this["email"];
|
const email = this["email"];
|
||||||
const domains = this["domains"];
|
const domains = this["domains"];
|
||||||
|
@ -331,63 +154,6 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
|
||||||
throw e;
|
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();
|
new CertApplyPlugin();
|
||||||
|
|
|
@ -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();
|
|
@ -1 +1,2 @@
|
||||||
export * from "./cert-plugin/index.js";
|
export * from "./cert-plugin/index.js";
|
||||||
|
export * from "./cert-plugin/lego.js";
|
||||||
|
|
|
@ -28,18 +28,22 @@ export class AsyncSsh2Client {
|
||||||
async connect() {
|
async connect() {
|
||||||
this.logger.info(`开始连接,${this.connConf.host}:${this.connConf.port}`);
|
this.logger.info(`开始连接,${this.connConf.host}:${this.connConf.port}`);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const conn = new ssh2.Client();
|
try {
|
||||||
conn
|
const conn = new ssh2.Client();
|
||||||
.on('error', (err: any) => {
|
conn
|
||||||
this.logger.error('连接失败', err);
|
.on('error', (err: any) => {
|
||||||
reject(err);
|
this.logger.error('连接失败', err);
|
||||||
})
|
reject(err);
|
||||||
.on('ready', () => {
|
})
|
||||||
this.logger.info('连接成功');
|
.on('ready', () => {
|
||||||
this.conn = conn;
|
this.logger.info('连接成功');
|
||||||
resolve(this.conn);
|
this.conn = conn;
|
||||||
})
|
resolve(this.conn);
|
||||||
.connect(this.connConf);
|
})
|
||||||
|
.connect(this.connConf);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async getSftp() {
|
async getSftp() {
|
||||||
|
@ -96,9 +100,7 @@ export class AsyncSsh2Client {
|
||||||
.stderr.on('data', (ret: Buffer) => {
|
.stderr.on('data', (ret: Buffer) => {
|
||||||
const err = this.convert(ret);
|
const err = this.convert(ret);
|
||||||
data += err;
|
data += err;
|
||||||
this.logger.info(
|
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
|
||||||
`[${this.connConf.host}][error]: ` + err.trimEnd()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -154,11 +156,7 @@ export class SshClient {
|
||||||
}
|
}
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
async uploadFiles(options: {
|
async uploadFiles(options: { connectConf: SshAccess; transports: any; mkdirs: boolean }) {
|
||||||
connectConf: SshAccess;
|
|
||||||
transports: any;
|
|
||||||
mkdirs: boolean;
|
|
||||||
}) {
|
|
||||||
const { connectConf, transports, mkdirs } = options;
|
const { connectConf, transports, mkdirs } = options;
|
||||||
await this._call({
|
await this._call({
|
||||||
connectConf,
|
connectConf,
|
||||||
|
@ -172,9 +170,7 @@ export class SshClient {
|
||||||
if (conn.windows) {
|
if (conn.windows) {
|
||||||
if (filePath.indexOf('/') > -1) {
|
if (filePath.indexOf('/') > -1) {
|
||||||
this.logger.info('--------------------------');
|
this.logger.info('--------------------------');
|
||||||
this.logger.info(
|
this.logger.info('请注意:windows下,文件目录分隔应该写成\\而不是/');
|
||||||
'请注意:windows下,文件目录分隔应该写成\\而不是/'
|
|
||||||
);
|
|
||||||
this.logger.info('--------------------------');
|
this.logger.info('--------------------------');
|
||||||
}
|
}
|
||||||
const spec = await conn.exec('echo %COMSPEC%');
|
const spec = await conn.exec('echo %COMSPEC%');
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue