Merge branch 'certd:v2' into v2

This commit is contained in:
origami
2024-11-16 23:32:28 +08:00
committed by GitHub
289 changed files with 2673 additions and 7088 deletions
@@ -9,7 +9,6 @@ export * from './plugin-other/index.js';
export * from './plugin-west/index.js';
export * from './plugin-doge/index.js';
export * from './plugin-qiniu/index.js';
export * from './plugin-jdcloud/index.js';
export * from './plugin-woai/index.js';
export * from './plugin-cachefly/index.js';
export * from './plugin-gcore/index.js';
@@ -1,5 +1,7 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { ILogger } from '@certd/basic';
import { AliyunAccess, AliyunClient } from '@certd/plugin-plus';
@IsDnsProvider({
@@ -1,6 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs';
import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
@IsTaskPlugin({
name: 'DeployCertToAliyunCDN',
title: '部署证书至阿里云CDN',
@@ -15,29 +16,35 @@ import { AliyunAccess, AliyunClient } from "@certd/plugin-plus";
})
export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
@TaskInput({
title: 'CDN加速域名',
helper: '你在阿里云上配置的CDN加速域名,比如:certd.docmirror.cn',
title: '证书服务接入点',
helper: '不会选就按默认',
value: 'cas.aliyuncs.com',
component: {
name: 'a-select',
options: [
{ value: 'cas.aliyuncs.com', label: '中国大陆' },
{ value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡' },
{ value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)' },
],
},
required: true,
})
domainName!: string;
@TaskInput({
title: '证书名称',
helper: '上传后将以此名称作为前缀备注',
})
certName!: string;
endpoint!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
from: ['CertApply', 'CertApplyLego', 'uploadCertToAliyun'],
},
required: true,
})
cert!: string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
@@ -49,47 +56,84 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: 'CDN加速域名',
helper: '你在阿里云上配置的CDN加速域名,比如:certd.docmirror.cn',
typeName: 'DeployCertToAliyunCDN',
action: DeployCertToAliyunCDN.prototype.onGetDomainList.name,
watches: ['certDomains', 'accessId'],
required: true,
})
)
domainName!: string | string[];
@TaskInput({
title: '证书名称',
helper: '上传后将以此名称作为前缀备注',
})
certName!: string;
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到阿里云cdn');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const access = await this.accessService.getById<AliyunAccess>(this.accessId);
const sslClient = new AliyunSslClient({
access,
logger: this.logger,
endpoint: this.endpoint || 'cas.aliyuncs.com',
});
let certId: any = this.cert;
if (typeof this.cert === 'object') {
certId = await sslClient.uploadCert({
name: this.appendTimeSuffix('certd'),
cert: this.cert,
});
}
const client = await this.getClient(access);
const params = await this.buildParams();
await this.doRequest(client, params);
if (typeof this.domainName === 'string') {
this.domainName = [this.domainName];
}
for (const domain of this.domainName) {
await this.SetCdnDomainSSLCertificate(client, {
CertId: certId,
DomainName: domain,
});
}
this.logger.info('部署完成');
}
async getClient(access: AliyunAccess) {
const client = new AliyunClient({logger:this.logger})
const client = new AliyunClient({ logger: this.logger });
await client.init({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://cdn.aliyuncs.com',
apiVersion: '2018-05-10',
})
return client
});
return client;
}
async buildParams() {
const CertName = (this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss');
const cert: any = this.cert;
return {
DomainName: this.domainName,
SSLProtocol: 'on',
CertName: CertName,
CertType: 'upload',
SSLPub: cert.crt,
SSLPri: cert.key,
};
}
async doRequest(client: any, params: any) {
async SetCdnDomainSSLCertificate(client: any, params: { CertId: number; DomainName: string }) {
const requestOption = {
method: 'POST',
formatParams: false,
};
const ret: any = await client.request('SetCdnDomainSSLCertificate', params, requestOption);
const ret: any = await client.request(
'SetCdnDomainSSLCertificate',
{
SSLProtocol: 'on',
...params,
},
requestOption
);
this.checkRet(ret);
this.logger.info('设置cdn证书成功:', ret.RequestId);
this.logger.info(`设置CDN: ${params.DomainName} 证书成功:`, ret.RequestId);
}
checkRet(ret: any) {
@@ -97,5 +141,39 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
throw new Error('执行失败:' + ret.Message);
}
}
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
const access = await this.accessService.getById<AliyunAccess>(this.accessId);
const client = await this.getClient(access);
const params = {
// 'DomainName': 'aaa',
PageSize: 500,
};
const requestOption = {
method: 'POST',
formatParams: false,
};
const res = await client.request('DescribeUserDomains', params, requestOption);
this.checkRet(res);
const pageData = res?.Domains?.PageData;
if (!pageData || pageData.length === 0) {
throw new Error('找不到CDN域名,您可以手动输入');
}
const options = pageData.map((item: any) => {
return {
value: item.DomainName,
label: item.DomainName,
domain: item.DomainName,
};
});
return optionsUtils.buildGroupOptions(options, this.certDomains);
}
}
new DeployCertToAliyunCDN();
@@ -0,0 +1,154 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AliyunAccess } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
@IsTaskPlugin({
name: 'DeployCertToAliyunOSS',
title: '部署证书至阿里云OSS',
icon: 'ant-design:aliyun-outlined',
group: pluginGroups.aliyun.key,
desc: '自动部署域名证书至阿里云OSS',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
@TaskInput({
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;
@TaskInput({
title: 'Bucket',
helper: '存储桶名称',
required: true,
})
bucket!: string;
@TaskInput({
title: '绑定的域名',
helper: '你在阿里云OSS上绑定的域名,比如:certd.docmirror.cn',
required: true,
})
domainName!: string;
@TaskInput({
title: '证书名称',
helper: '上传后将以此名称作为前缀备注',
})
certName!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
},
required: true,
})
cert!: CertInfo;
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
component: {
name: 'access-selector',
type: 'aliyun',
},
required: true,
})
accessId!: string;
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到阿里云OSS');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
this.logger.info(`bucket: ${this.bucket}, region: ${this.region}, domainName: ${this.domainName}`);
const client = await this.getClient(access);
await this.doRequest(client, {});
this.logger.info('部署完成');
}
async getClient(access: AliyunAccess) {
// @ts-ignore
const OSS = await import('ali-oss');
return new OSS.default({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: this.region,
//@ts-ignore
authorizationV4: true,
// yourBucketName填写Bucket名称。
bucket: this.bucket,
});
}
async doRequest(client: any, params: any) {
params = client._bucketRequestParams('POST', this.bucket, {
cname: '',
comp: 'add',
});
const xml = `
<BucketCnameConfiguration>
<Cname>
<Domain>${this.domainName}</Domain>
<CertificateConfiguration>
<PrivateKey>${this.cert.key}</PrivateKey>
<Certificate>${this.cert.crt}</Certificate>
</CertificateConfiguration>
</Cname>
</BucketCnameConfiguration>`;
params.content = xml;
params.mime = 'xml';
params.successStatuses = [200];
const res = await client.request(params);
this.checkRet(res);
return res;
}
checkRet(ret: any) {
if (ret.code != null) {
throw new Error('执行失败:' + ret.Message);
}
}
}
new DeployCertToAliyunOSS();
@@ -1,3 +1,4 @@
export * from './deploy-to-cdn/index.js';
export * from './deploy-to-dcdn/index.js';
export * from './deploy-to-oss/index.js';
export * from './upload-to-aliyun/index.js';
@@ -1,5 +1,7 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { HttpClient, ILogger } from '@certd/basic';
import { CloudflareAccess } from './access.js';
export type CloudflareRecord = {
@@ -1,7 +1,9 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { HttpClient, ILogger } from '@certd/basic';
import { DemoAccess } from './access.js';
import { isDev } from "../../utils/env.js";
import { isDev } from '../../utils/env.js';
type DemoRecord = {
// 这里定义Record记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到id就行,用于删除txt解析记录,清理申请痕迹
@@ -1,22 +1,26 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { isDev } from '../../../utils/env.js';
import { isDev } from '@certd/basic';
@IsTaskPlugin({
name: 'demoTest',
title: 'Demo测试插件',
icon: 'clarity:plugin-line',
//插件分组
group: pluginGroups.other.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
// 你开发的插件要删除此项,否则不会在生产环墋中显示
deprecated: isDev() ? '测试插件,生产环境不显示' : undefined,
})
export class DemoTestPlugin extends AbstractTaskPlugin {
//测试参数
@TaskInput({
title: '属性示例',
value: '默认值',
component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/input-cn
name: 'a-input',
@@ -57,7 +61,7 @@ export class DemoTestPlugin extends AbstractTaskPlugin {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
},
// required: true,
// required: true, // 必填
})
cert!: CertInfo;
@@ -70,6 +74,7 @@ export class DemoTestPlugin extends AbstractTaskPlugin {
type: 'demo', //固定授权类型
},
// rules: [{ required: true, message: '此项必填' }],
// required: true, //必填
})
accessId!: string;
@@ -98,8 +103,5 @@ export class DemoTestPlugin extends AbstractTaskPlugin {
this.logger.info('授权id:', accessId);
}
}
//TODO 这里实例化插件,进行注册
if (isDev()) {
//你的实现 要去掉这个if,不然生产环境将不会显示
new DemoTestPlugin();
}
//实例化一下,注册插件
new DemoTestPlugin();
@@ -1,7 +1,7 @@
import crypto from 'crypto';
import querystring from 'querystring';
import { DogeCloudAccess } from '../access.js';
import { HttpClient } from '@certd/pipeline';
import { HttpClient } from '@certd/basic';
export class DogeClient {
accessKey: string;
@@ -1,28 +0,0 @@
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'dynadot',
title: 'dynadot授权',
desc: '************\n注意:申请证书时会覆盖已有的域名解析配置,慎用\n************\n待优化,主要是dynadot的接口一言难尽',
})
export class DynadotAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: 'API Production Key',
component: {
placeholder: '授权key',
},
helper: '前往 [Dynadot API](https://www.dynadot.com/account/domain/setting/api.html) 获取 API Production Key',
required: true,
encrypt: true,
})
apiProductionKey = '';
}
new DynadotAccess();
@@ -1,94 +0,0 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { DynadotAccess } from './access.js';
import querystring from 'querystring';
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: 'dynadot',
title: 'dynadot',
desc: 'dynadot dns provider',
// 这里是对应的 cloudflare的access类型名称
accessType: 'dynadot',
deprecated: '暂不支持',
})
export class DynadotDnsProvider extends AbstractDnsProvider {
// 通过Autowire传递context
@Autowire()
logger!: ILogger;
access!: DynadotAccess;
async onInstance() {
this.access = this.ctx.access as DynadotAccess;
}
private async doRequest(command: string, query: any) {
const baseUrl = 'https://api.dynadot.com/api3.json?key=' + this.access.apiProductionKey;
const qs = querystring.stringify(query);
const url = `${baseUrl}&command=${command}&${qs}`;
const res = await this.ctx.http.request<any, any>({
url,
method: 'get',
headers: {
'Content-Type': 'application/json',
},
});
/*
"SetDnsResponse": {
"ResponseCode": 0,
"Status": "success"
}
*/
for (const resKey in res) {
if (res[resKey].ResponseCode != null && res[resKey].ResponseCode !== 0) {
throw new Error(`请求失败:${res[resKey].Status}`);
}
}
return res;
}
/**
* 创建dns解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions) {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
//先获取域名原始解析记录
//https://api.dynadot.com/api3.xml?key=[API Key]&command=domain_info&domain=domain1.com
const res1 = await this.doRequest('domain_info', {
domain: domain,
});
// this.logger.info(`域名信息:${JSON.stringify(res1)}`);
// "DomainInfoResponse.NameServerSettings":{"Type":"Dynadot DNS","SubDomains":[{"Subhost":"_acme-challenge","RecordType":"TXT","Value":"43XrhFA6pJpE7a-20y7BmC6CsN20TMt5l-Zl-CL_-4I"}],"TTL":"300"}
this.logger.info('原始域名解析记录:', JSON.stringify(res1.DomainInfoResponse?.DomainInfo?.NameServerSettings));
const prefix = fullRecord.replace(`.${domain}`, '');
// 给domain下创建txt类型的dns解析记录,fullRecord
const res = await this.doRequest('set_dns2', {
domain: domain,
subdomain0: prefix,
sub_record_type0: 'TXT',
sub_record0: value,
});
this.logger.info(`添加域名解析成功:fullRecord=${fullRecord},value=${value}`);
this.logger.info(`请求结果:${JSON.stringify(res)}`);
//本接口需要返回本次创建的dns解析记录,这个记录会在删除的时候用到
return {};
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<any>): Promise<void> {}
}
//实例化这个provider,将其自动注册到系统中
new DynadotDnsProvider();
@@ -1,3 +0,0 @@
export * from './dns-provider.js';
export * from './plugins/index.js';
export * from './access.js';
@@ -2,7 +2,7 @@
import ssh2, { ConnectConfig, ExecOptions } from 'ssh2';
import path from 'path';
import * as _ from 'lodash-es';
import { ILogger } from '@certd/pipeline';
import { ILogger } from '@certd/basic';
import { SshAccess } from '../access/index.js';
import stripAnsi from 'strip-ansi';
import { SocksClient } from 'socks';
@@ -86,6 +86,7 @@ export class AsyncSsh2Client {
sftp.fastPut(localPath, remotePath, (err: Error) => {
if (err) {
reject(err);
this.logger.error('请确认路径是否包含文件名,路径本身不能是目录,路径不能有*?之类的特殊符号,要有写入权限');
return;
}
this.logger.info(`上传文件成功:${localPath} => ${remotePath}`);
@@ -17,12 +17,47 @@ import path from 'path';
},
})
export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
},
required: true,
})
cert!: CertInfo;
@TaskInput({
title: '证书类型',
helper: '要部署的证书格式,支持pem、pfx、der、jks格式',
component: {
name: 'a-select',
options: [
{ value: 'pem', label: 'pem,用于Nginx等大部分应用' },
{ value: 'pfx', label: 'pfx,一般用于IIS' },
{ value: 'der', label: 'der,一般用于Apache' },
{ value: 'jks', label: 'jks,一般用于JAVA应用' },
],
},
required: true,
})
certType!: string;
@TaskInput({
title: '证书保存路径',
helper: '全链证书,路径要包含文件名' + '\n推荐使用相对路径,将写入与数据库同级目录,无需映射,例如:tmp/cert.pem',
component: {
placeholder: 'tmp/full_chain.pem',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
crtPath!: string;
@@ -32,6 +67,14 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
component: {
placeholder: 'tmp/cert.key',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
keyPath!: string;
@@ -42,6 +85,13 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
component: {
placeholder: '/root/deploy/nginx/intermediate.pem',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
})
}
`,
rules: [{ type: 'filepath' }],
})
icPath!: string;
@@ -52,6 +102,14 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
component: {
placeholder: 'tmp/cert.pfx',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pfx';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
pfxPath!: string;
@@ -63,30 +121,35 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
component: {
placeholder: 'tmp/cert.der 或 tmp/cert.cer',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'der';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
derPath!: string;
// @TaskInput({
// title: 'jks证书保存路径',
// helper: '用于java,路径要包含文件名,例如:tmp/cert.jks',
// component: {
// placeholder: 'tmp/cert.jks',
// },
// rules: [{ type: 'filepath' }],
// })
jksPath!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
title: 'jks证书保存路径',
helper: '用于java,路径要包含文件名,例如:tmp/cert.jks',
component: {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
placeholder: 'tmp/cert.jks',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'jks';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
cert!: CertInfo;
jksPath!: string;
@TaskOutput({
title: '证书保存路径',
@@ -18,12 +18,47 @@ import dayjs from 'dayjs';
},
})
export class UploadCertToHostPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
},
required: true,
})
cert!: CertInfo;
@TaskInput({
title: '证书格式',
helper: '要部署的证书格式,支持pem、pfx、der、jks',
component: {
name: 'a-select',
options: [
{ value: 'pem', label: 'pemNginx等大部分应用' },
{ value: 'pfx', label: 'pfx,一般用于IIS' },
{ value: 'der', label: 'der,一般用于Apache' },
{ value: 'jks', label: 'jks,一般用于JAVA应用' },
],
},
required: true,
})
certType!: string;
@TaskInput({
title: '证书保存路径',
helper: '全链证书,需要有写入权限,路径要包含证书文件名,例如:/tmp/cert.pem',
helper: '填写应用原本的证书保存路径,路径要包含证书文件名,例如:/tmp/cert.pem',
component: {
placeholder: '/root/deploy/nginx/full_chain.pem',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
crtPath!: string;
@@ -33,6 +68,14 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
component: {
placeholder: '/root/deploy/nginx/cert.key',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
keyPath!: string;
@@ -43,50 +86,70 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
component: {
placeholder: '/root/deploy/nginx/intermediate.pem',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
})
}
`,
rules: [{ type: 'filepath' }],
})
icPath!: string;
@TaskInput({
title: 'PFX证书保存路径',
helper: '用于IIS证书部署,需要有写入权限,路径要包含证书文件名,例如:/tmp/cert.pfx',
helper: '填写应用原本的证书保存路径,路径要包含证书文件名,例如:D:\\iis\\cert.pfx',
component: {
placeholder: '/root/deploy/nginx/cert.pfx',
placeholder: 'D:\\iis\\cert.pfx',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pfx';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
pfxPath!: string;
@TaskInput({
title: 'DER证书保存路径',
helper: '用于Apache证书部署,需要有写入权限,路径要包含证书文件名,例如:/tmp/cert.der',
helper: '填写应用原本的证书保存路径,路径要包含证书文件名,例如:/tmp/cert.der',
component: {
placeholder: '/root/deploy/nginx/cert.der',
placeholder: '/root/deploy/apache/cert.der',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'der';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
derPath!: string;
// @TaskInput({
// title: 'jks证书保存路径',
// helper: '需要有写入权限,路径要包含证书文件名,例如:/tmp/cert.jks',
// component: {
// placeholder: '/root/deploy/nginx/cert.jks',
// },
// rules: [{ type: 'filepath' }],
// })
jksPath!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
title: 'jks证书保存路径',
helper: '填写应用原本的证书保存路径,路径要包含证书文件名,例如:/tmp/cert.jks',
component: {
name: 'output-selector',
from: ['CertApply', 'CertApplyLego'],
placeholder: '/root/deploy/java_app/cert.jks',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'jks';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
cert!: CertInfo;
jksPath!: string;
@TaskInput({
title: '主机登录配置',
@@ -1,6 +1,8 @@
import * as _ from 'lodash-es';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { ILogger } from '@certd/basic';
import { HuaweiAccess } from '../access/index.js';
import { ApiRequestOptions, HuaweiYunClient } from '@certd/lib-huawei';
@@ -1,7 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, resetLogConfigure, RunStrategy, TaskInput } from '@certd/pipeline';
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { HuaweiAccess } from '../../access/index.js';
import { CertInfo } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { resetLogConfigure } from '@certd/basic';
@IsTaskPlugin({
name: 'HauweiDeployCertToCDN',
@@ -1,39 +0,0 @@
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'jdcloud',
title: '京东云授权',
desc: '暂时无法成功申请,还没测试通过',
})
export class JDCloudAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: 'AccessKeyId',
component: {
placeholder: 'AK',
},
helper: '前往 [AccessKey管理](https://uc.jdcloud.com/account/accesskey) 获取 API Production Key',
required: true,
encrypt: true,
})
accessKeyId = '';
@AccessInput({
title: 'AccessKeySecret',
component: {
placeholder: 'SK',
},
helper: 'SK',
required: true,
encrypt: true,
})
accessKeySecret = '';
}
new JDCloudAccess();
@@ -1,124 +0,0 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { JDCloudAccess } from './access.js';
function promisfy(func: any) {
return (params: any, regionId: string) => {
return new Promise((resolve, reject) => {
try {
func(params, regionId, (err, result) => {
if (err) {
reject(err.error || err);
return;
}
resolve(result);
});
} catch (e) {
reject(e);
}
});
};
}
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: 'jdcloud',
title: '京东云',
desc: '京东云 dns provider',
// 这里是对应的 cloudflare的access类型名称
accessType: 'jdcloud',
deprecated: '暂不支持',
})
export class JDCloudDnsProvider extends AbstractDnsProvider {
// 通过Autowire传递context
@Autowire()
logger!: ILogger;
access!: JDCloudAccess;
service!: any;
regionId: string;
async onInstance() {
this.access = this.ctx.access as JDCloudAccess;
const { DomainService } = await import('@certd/lib-jdcloud');
// @ts-ignore
this.regionId = 'cn-north-1';
this.service = new DomainService({
credentials: {
accessKeyId: this.access.accessKeyId,
secretAccessKey: this.access.accessKeySecret,
},
regionId: this.regionId,
});
}
/**
* 创建dns解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions) {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
const describeDomains = promisfy((a, b, c) => {
this.service.describeDomains(a, b, c);
});
const res: any = await describeDomains({ domainName: domain, pageNumber: 1, pageSize: 10 }, this.regionId);
this.logger.info('请求成功:', JSON.stringify(res.result));
const dataList = res.result.dataList;
if (dataList.length === 0) {
throw new Error('账号下找不到域名:' + domain);
}
const domainId = dataList[0].id;
this.logger.info('domainId:', domainId);
//开始创建解析记录
const createResourceRecord = promisfy((a, b, c) => {
this.service.createResourceRecord(a, b, c);
});
const res2: any = await createResourceRecord(
{
domainId,
req: {
hostRecord: fullRecord,
hostValue: value,
type: 'TXT',
},
},
this.regionId
);
this.logger.info('请求成功:', JSON.stringify(res.result));
const recordList = res2.result.dataList;
const recordId = recordList[0].id;
this.logger.info(`添加域名解析成功:fullRecord=${fullRecord},value=${value}`);
this.logger.info(`请求结果:recordId:${recordId}`);
//本接口需要返回本次创建的dns解析记录,这个记录会在删除的时候用到
return { id: recordId, domainId };
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<any>): Promise<void> {
// const { fullRecord, value, domain } = options.recordReq;
const record = options.recordRes;
const deleteResourceRecord = promisfy(this.service.deleteResourceRecord);
const res: any = await deleteResourceRecord(
{
domainId: record.domainId,
resourceRecordId: record.id,
},
this.regionId
);
this.logger.info(`删除dns解析记录成功:${JSON.stringify(res)}`);
}
}
//实例化这个provider,将其自动注册到系统中
new JDCloudDnsProvider();
@@ -1,3 +0,0 @@
export * from './dns-provider.js';
export * from './plugins/index.js';
export * from './access.js';
@@ -21,7 +21,7 @@ export type CustomScriptContext = {
export class CustomScriptPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '脚本',
helper: '自定义js脚本',
helper: '自定义js脚本[脚本编写帮助文档](https://certd.docmirror.cn/guide/use/custom-script/)',
component: {
name: 'a-textarea',
vModel: 'value',
@@ -1,4 +1,6 @@
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { HttpClient, ILogger } from '@certd/basic';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import * as _ from 'lodash-es';
import { DnspodAccess } from '../access/index.js';
@@ -1,4 +1,6 @@
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { HttpClient, ILogger } from '@certd/basic';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { TencentAccess } from '@certd/plugin-plus';
@@ -1,6 +1,6 @@
import { TencentAccess } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
import { ILogger } from '@certd/pipeline';
import { ILogger } from '@certd/basic';
export class TencentSslClient {
access: TencentAccess;
logger: ILogger;
@@ -94,14 +94,15 @@ export class TencentDeleteExpiringCert extends AbstractPlusTaskPlugin {
};
const res = await sslClient.DescribeCertificates(params);
let certificates = res?.Certificates;
if (!certificates && !certificates.length) {
if (!certificates && certificates.length === 0) {
this.logger.info('没有找到证书');
return;
}
const lastDay = dayjs().add(this.expiringDays, 'day');
certificates = certificates.filter((item: any) => {
const endTime = item.CertEndTime;
return dayjs(endTime).add(this.expiringDays, 'day').isBefore(dayjs());
return dayjs(endTime).isBefore(lastDay);
});
for (const certificate of certificates) {
this.logger.info(`证书ID:${certificate.CertificateId}, 过期时间:${certificate.CertEndTime}Alias:${certificate.Alias},证书域名:${certificate.Domain}`);
@@ -1,4 +1,4 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { TencentAccess } from '@certd/plugin-plus';
import dayjs from 'dayjs';
@@ -131,10 +131,10 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
}
try {
await utils.sleep(2000);
await this.ctx.utils.sleep(2000);
let newCertId = await this.getCertIdFromProps(client);
if ((lastCertId && newCertId === lastCertId) || (!lastCertId && !newCertId)) {
await utils.sleep(2000);
await this.ctx.utils.sleep(2000);
newCertId = await this.getCertIdFromProps(client);
}
if (newCertId === lastCertId) {
@@ -1,12 +1,12 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { AbstractPlusTaskPlugin, createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { createRemoteSelectInputDefine } from '@certd/plugin-plus';
import { TencentSslClient } from '../../lib/index.js';
@IsTaskPlugin({
name: 'DeployCertToTencentCosPlugin',
title: '部署证书到腾讯云COS',
needPlus: true,
needPlus: false,
icon: 'svg:icon-tencentcloud',
group: pluginGroups.tencent.key,
desc: '部署到腾讯云COS源站域名证书【注意:很不稳定,需要重试很多次偶尔才能成功一次】',
@@ -16,7 +16,7 @@ import { TencentSslClient } from '../../lib/index.js';
},
},
})
export class DeployCertToTencentCosPlugin extends AbstractPlusTaskPlugin {
export class DeployCertToTencentCosPlugin extends AbstractTaskPlugin {
/**
* AccessProvider的id
*/
@@ -1,5 +1,8 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, HttpClient, ILogger } from '@certd/pipeline';
import { Autowire } from '@certd/pipeline';
import { HttpClient, ILogger } from '@certd/basic';
import { WestAccess } from './access.js';
type westRecord = {
@@ -1,6 +1,7 @@
import { AbstractTaskPlugin, HttpClient, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { WoaiAccess } from '../access.js';
import { HttpClient } from '@certd/basic';
@IsTaskPlugin({
name: 'WoaiCDN',