diff --git a/packages/ui/certd-client/src/views/framework/login/index.vue b/packages/ui/certd-client/src/views/framework/login/index.vue index 85124081f..1b971806a 100644 --- a/packages/ui/certd-client/src/views/framework/login/index.vue +++ b/packages/ui/certd-client/src/views/framework/login/index.vue @@ -131,7 +131,7 @@ export default defineComponent({ phoneCode: "86", mobile: "", password: "", - loginType: urlLoginType || settingStore.sysPublic.defaultLoginType, //password + loginType: urlLoginType || defaultLoginType, //password smsCode: "", captcha: null, smsCaptcha: null, diff --git a/packages/ui/certd-server/.env b/packages/ui/certd-server/.env index 345247b4a..2a245bd94 100644 --- a/packages/ui/certd-server/.env +++ b/packages/ui/certd-server/.env @@ -1,2 +1,2 @@ LEGO_VERSION=4.30.1 -certd_plugin_loadmode=metadata \ No newline at end of file +certd_plugin_loadmode=dev \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index d4d275ee5..1644c5952 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -43,4 +43,5 @@ export * from './plugin-ucloud/index.js' export * from './plugin-goedge/index.js' export * from './plugin-lib/index.js' export * from './plugin-plus/index.js' -export * from './plugin-cert/index.js' \ No newline at end of file +export * from './plugin-cert/index.js' +export * from './plugin-zenlayer/index.js' \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts new file mode 100644 index 000000000..495e77180 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts @@ -0,0 +1,377 @@ +import { HttpRequestConfig } from '@certd/basic'; +import { IsAccess, AccessInput, BaseAccess, PageSearch } from '@certd/pipeline'; +import qs from 'qs'; + +export type ZenlayerRequest = HttpRequestConfig & { + action: string; + version?: string; +} +/** + * 这个注解将注册一个授权配置 + * 在certd的后台管理系统中,用户可以选择添加此类型的授权 + */ +@IsAccess({ + name: 'zenlayer', + title: 'Zenlayer授权', + icon: 'svg:icon-zenlayer', + desc: 'Zenlayer授权', +}) +export class ZenlayerAccess extends BaseAccess { + + /** + * 授权属性配置 + */ + @AccessInput({ + title: 'AccessKeyId', + component: { + placeholder: '访问密钥ID', + }, + helper: "[访问密钥管理](https://console.zenlayer.com/accessKey)获取", + required: true, + encrypt: false, + }) + accessKeyId = ''; + + + /** + * 授权属性配置 + */ + @AccessInput({ + title: 'AccessKey Password', + component: { + placeholder: '访问密钥密码', + }, + required: true, + encrypt: true, + }) + accessKeyPassword = ''; + + + + + @AccessInput({ + title: "测试", + component: { + name: "api-test", + action: "TestRequest" + }, + helper: "点击测试接口是否正常" + }) + testRequest = true; + + client: any; + + async onTestRequest() { + await this.getCertList(); + return "ok"; + } + + + async getCertList(req: PageSearch = {}) { + const pageNo = req.pageNo ?? 1; + const pageSize = req.pageSize ?? 100; + const res = await this.doRequest({ + url: "/", + baseURL: "https://console.zenlayer.com/api/v2/cdn", + action: "DescribeCertificates", + data: { + PageNum: pageNo, + PageSize: pageSize + } + }); + return res; + } + + + /** + * 申请安全凭证 +本文使用的安全凭证为密钥,密钥包括 accessKeyId 和 accessKeyPassword。 + +AccessKeyId:用于标识 API 调用者身份,可以简单类比为用户名。 + +AccessKeyPassword:用于验证 API 调用者的身份,可以简单类比为密码。 + +用户必须严格保管安全凭证,避免泄露,否则将危及财产安全。如已泄漏,请立刻禁用该安全凭证。 + +你可以根据Zenlayer的用户指南文档来获取你的安全凭证。 + +签名过程v2 +Zenlayer Open API V2 支持 Post 请求,仅支持 Content-Type: application/json。 接口使用json格式进行调用。 + +下面以裸机云查询实例列表为例: + + +复制 +curl -X POST https://console.zenlayer.com/api/v2/bmc \ +-H "Authorization: ZC2-HMAC-SHA256 Credential=0D9UtpyKYcHxms5v, SignedHeaders=content-type;host, Signature=efb356c32e55c781e10dc676da59462c22596d82e91c57803666243379555b2f" \ +-H "Content-Type: application/json; charset=utf-8" \ +-H "X-ZC-Action: DescribeInstances" \ +-H "X-ZC-Timestamp: 1673361177" \ +-H "X-ZC-Signature-Method: ZC2-HMAC-SHA256" \ +-H "X-ZC-Version: 2022-11-20" \ +-d '{"pageSize":10,"pageNum":1,"zoneId":"HKG-A"}' +Request Headers: + +Key +说明 +示例 +X-ZC-Timestamp + +请求的时间戳,精确到秒 + +1673361177 + +X-ZC-Version + +请求的API版本 + +2022-11-20 + +X-ZC-Action + +请求的动作 + +DescribeInstances + +X-ZC-Signature-Method + +签名方法 + +ZC2-HMAC-SHA256 + +Authorization + +签名认证 + +1. 拼接规范请求串 +按如下伪代码格式拼接规范请求串(CanonicalRequest): + + +复制 +CanonicalRequest = + HTTPRequestMethod + '\n' + + CanonicalURI + '\n' + + CanonicalQueryString + '\n' + + CanonicalHeaders + '\n' + + SignedHeaders + '\n' + + HexEncode(Hash(RequestPayload)) +字段名称 +解释 +HTTPRequestMethod + +HTTP 请求方法。 + +固定为POST。 + +CanonicalURI + +URI 参数。 + +API 固定为正斜杠(/)。 + +CanonicalQueryString + +发起 HTTP 请求 URL 中的查询字符串。 + +对于 POST 请求,固定为空字符串""。 + +CanonicalHeaders + +参与签名的头部信息,可加入自定义的头部参与签名以提高自身请求的唯一性和安全性。 + +拼接规则:头部 key 和 value 统一转成小写,并去掉首尾空格,按照 key:value\n 格式拼接(注意最后包含'\n');多个头部,按照头部 key(小写)的 ASCII 升序进行拼接。此示例计算结果是: + +content-type:application/json; charset=utf-8\nhost:console.zenlayer.com。 + +SignedHeaders + +参与签名的头部信息。 + +说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。content-type 和 host 为必选头部。 拼接规则:头部 key 统一转成小写;多个头部 key(小写)按照 ASCII 升序进行拼接,并且以分号(;)分隔。此示例为content-type;host。 + +HashedRequestPayload + +请求正文(payload,即 body)。 + +此示例为{"pageSize":10,"pageNum":1,"zoneId":"HKG-A"})的哈希值,计算伪代码为 HexEncode(Hash(RequestPayload)),即对 HTTP 请求正文做 SHA256 哈希,然后十六进制编码。此示例的计算结果是 :5f714687ba91c606d503467766151206392474accd137ffea6dce2420b67c29a。 + +2. 拼接待签字符串 + +复制 +StringToSign = + Algorithm + \n + # 指定签名算法。对于 SHA256,算法为 ZC2-HMAC-SHA256。 + RequestDateTime + \n + # 指定请求时间戳。 + HashedCanonicalRequest +字段名称 +解释 +Algorithm + +签名算法。 + +目前固定为 ZC2-HMAC-SHA256。 + +RequestTimestamp + +请求时间戳。 + +即请求头部的公共参数 X-ZC-Timestamp 取值,取当前时间 UNIX 时间戳,精确到秒。此示例取值为1673361177。 + +HashedCanonicalRequest + +前述步骤拼接所得规范请求串的哈希值。 + +计算伪代码为 Lowercase(HexEncode(Hash.SHA256(CanonicalRequest)))。此示例计算结果是: 29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee。 + +根据以上规则,示例中得到的待签名字符串如下: + + +复制 +ZC2-HMAC-SHA256 +1673361177 +29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee +3. 基于 AK 和 StringToSign 计算出签名 +计算签名,伪代码如下: + + +复制 +Signature = HexEncode(HMAC_SHA256(AccessKeyPassword, StringToSign)) +字段名称 +解释 +AccessKeyPassword + +原始的 AccessKeyPassword。 + +如 Gu5t9xGARNpq86cd98joQYCN3。 + +StringToSign + +步骤二获得的结果。 + +4. 拼接 Authorization +按如下格式拼接 Authorization: + + +复制 +Authorization = + Algorithm + ' ' + + 'Credential=' + AccessKeyId + ', ' + + 'SignedHeaders=' + SignedHeaders + ', ' + + 'Signature=' + Signature +字段名称 +解释 +Algorithm + +签名算法。 + +目前为 ZC2-HMAC-SHA256。 + +AccessKeyId + +密钥对中的 AccessKeyId。 + +如 0D9UtpyKYcHxms5v。 + +SignedHeaders + +见上文,参与签名的头部信息。 + +此示例取值为 content-type;host。 + +Signature + +签名值。 + +根据以上方法,此示例计算结果是 efb356c32e55c781e10dc676da59462c22596d82e91c57803666243379555b2f。 + */ + + + async getAuthorizationHeaders(req: ZenlayerRequest) { + + /** + * CanonicalRequest = + HTTPRequestMethod + '\n' + + CanonicalURI + '\n' + + CanonicalQueryString + '\n' + + CanonicalHeaders + '\n' + + SignedHeaders + '\n' + + HexEncode(Hash(RequestPayload)) + */ + if (!req.headers) { + req.headers = {}; + } + if (!req.headers['content-type']) { + req.headers['content-type'] = "application/json; charset=utf-8"; + } + if (!req.headers['host']) { + req.headers['host'] = "console.zenlayer.com"; + } + + const CanonicalQueryString = req.method === 'POST' ? '' : qs.stringify(req.params); + const SignedHeaders = "content-type;host"; + const CanonicalHeaders = `${req.headers['content-type']}\n${req.headers['host']}`; + const HashedRequestPayload = this.ctx.utils.hash.sha256(JSON.stringify(req.data || {}), "hex"); + const CanonicalRequest = `${req.method}\n${req.url}\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`; + const timestamp = Math.floor(Date.now() / 1000); + const signMethod = "ZC2-HMAC-SHA256"; + + const StringToSign = `${signMethod}\n${timestamp}\n${this.accessKeyId}\n${CanonicalRequest}`; + + const signature = this.ctx.utils.hash.hmacSha256(StringToSign, "hex"); + + const authorization = `${signMethod} Credential=${this.accessKeyId}, SignedHeaders=${SignedHeaders}, Signature=${signature}`; + + + /** + * X-ZC-Timestamp + +请求的时间戳,精确到秒 + +1673361177 + +X-ZC-Version + +请求的API版本 + +2022-11-20 + +X-ZC-Action + +请求的动作 + +DescribeInstances + +X-ZC-Signature-Method + +签名方法 + +ZC2-HMAC-SHA256 + +Authorization + +签名认证 + */ + return { + ...req.headers, + 'X-ZC-Timestamp': timestamp.toString(), + 'X-ZC-Action': req.action, + 'X-ZC-Version': req.version || "2022-11-20", + 'X-ZC-Signature-Method': signMethod, + 'Authorization': authorization, + }; + } + + async doRequest(req: ZenlayerRequest) { + const headers = await this.getAuthorizationHeaders(req); + req.headers = headers + const res = await this.ctx.http.request({ + baseURL: req.baseURL || "https://console.zenlayer.com", + ...req + }); + this.ctx.logger.info(`doRequest ${req.url} ${res.statusCode} ${JSON.stringify(res.data)}`); + return res; + } + + +} + +new ZenlayerAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/client.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/client.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/index.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/index.ts new file mode 100644 index 000000000..c49d8bf88 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-zenlayer/index.ts @@ -0,0 +1,2 @@ +export * from './access.js'; +export * from './plugins/index.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/index.ts new file mode 100644 index 000000000..55ab7cf74 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/index.ts @@ -0,0 +1 @@ +export * from './plugin-refresh-cert.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts new file mode 100644 index 000000000..f63cca525 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts @@ -0,0 +1,108 @@ +import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; + +@IsTaskPlugin({ + //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 + name: "ZenlayerRefreshCert", + title: "Zenlayer-刷新证书", + desc: "将证书部署到Zenlayer CDN", + icon: "svg:icon-zenlayer", + //插件分组 + group: pluginGroups.cdn.key, + needPlus: false, + default: { + //默认值配置照抄即可 + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed + } + } +}) +//类名规范,跟上面插件名称(name)一致 +export class ZenlayerRefreshCert extends AbstractTaskPlugin { + //证书选择,此项必须要有 + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames] + } + // required: true, // 必填 + }) + cert!: CertInfo | { type: string, id: number, name: string }; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + //授权选择框 + @TaskInput({ + title: "Zenlayer授权", + component: { + name: "access-selector", + type: "zenlayer" //固定授权类型 + }, + required: true //必填 + }) + accessId!: string; + // + + @TaskInput( + createRemoteSelectInputDefine({ + title: "证书ID列表", + helper: "要更新的Zenlayer证书ID列表", + + action: ZenlayerRefreshCert.prototype.onGetCertList.name + }) + ) + certList!: string[]; + + //插件实例化时执行的方法 + async onInstance() { + } + + //插件执行方法 + async execute(): Promise { + // const access = await this.getAccess(this.accessId); + this.logger.info("部署完成"); + } + async onGetCertList(req: PageSearch = {}) { + // const access = await this.getAccess(this.accessId); + + // const pageNo = req.pageNo ?? 1; + // const pageSize = req.pageSize ?? 100; + // const res = await access.GetCertList( + // { + // PageNo: pageNo, + // PageSize: pageSize + // } + // ); + // const total = res.TotalCount; + // const list = res.DomainInfoList || []; + // if (!list || list.length === 0) { + // throw new Error("没有找到CDN域名,请先在控制台创建CDN域名"); + // } + + // /** + // * "Domain": "ucloud.certd.handsfree.work", + // "DomainId": "ucdn-1kwdtph5ygbb" + // */ + // const options = list.map((item: any) => { + // return { + // label: `${item.Domain}<${item.DomainId}>`, + // value: `${item.Domain}`, + // domain: item.Domain + // }; + // }); + // return { + // list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains), + // total: total, + // pageNo: pageNo, + // pageSize: pageSize + // }; + } + +} + +//实例化一下,注册插件 +new ZenlayerRefreshCert();