mirror of https://github.com/certd/certd
				
				
				
			perf: 支持部署到金山云CDN
							parent
							
								
									9e1e4eeec2
								
							
						
					
					
						commit
						dfa74a69f7
					
				| 
						 | 
				
			
			@ -11,7 +11,6 @@ export type PageSearch = {
 | 
			
		|||
  // sortOrder?: "asc" | "desc";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type PageRes = {
 | 
			
		||||
  pageNo?: number;
 | 
			
		||||
  pageSize?: number;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ const option = ref({
 | 
			
		|||
      center: ["60%", "50%"],
 | 
			
		||||
      name: "状态",
 | 
			
		||||
      type: "pie",
 | 
			
		||||
      radius: "80%",
 | 
			
		||||
      radius: ["30%", "70%"],
 | 
			
		||||
      avoidLabelOverlap: false,
 | 
			
		||||
      itemStyle: {
 | 
			
		||||
        borderRadius: 0,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,3 +31,4 @@ export * from './plugin-namesilo/index.js'
 | 
			
		|||
export * from './plugin-proxmox/index.js'
 | 
			
		||||
export * from './plugin-wangsu/index.js'
 | 
			
		||||
export * from './plugin-admin/index.js'
 | 
			
		||||
export * from './plugin-ksyun/index.js'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,110 @@
 | 
			
		|||
import {AccessInput, BaseAccess, IsAccess} from "@certd/pipeline";
 | 
			
		||||
import {KsyunClient} from './client.js'
 | 
			
		||||
import {CertInfo} from "@certd/plugin-cert";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 */
 | 
			
		||||
@IsAccess({
 | 
			
		||||
  name: "ksyun",
 | 
			
		||||
  title: "金山云授权",
 | 
			
		||||
  desc: "",
 | 
			
		||||
  icon: "svg:icon-ksyun"
 | 
			
		||||
})
 | 
			
		||||
export class KsyunAccess extends BaseAccess {
 | 
			
		||||
 | 
			
		||||
  @AccessInput({
 | 
			
		||||
    title: 'AccessKeyID',
 | 
			
		||||
    component: {
 | 
			
		||||
      placeholder: 'AccessKeyID',
 | 
			
		||||
    },
 | 
			
		||||
    helper: "[获取密钥](https://uc.console.ksyun.com/pro/iam/#/set/keyManage)",
 | 
			
		||||
    required: true,
 | 
			
		||||
  })
 | 
			
		||||
  accessKeyId = '';
 | 
			
		||||
  @AccessInput({
 | 
			
		||||
    title: 'AccessKeySecret',
 | 
			
		||||
    component: {
 | 
			
		||||
      placeholder: 'AccessKeySecret',
 | 
			
		||||
    },
 | 
			
		||||
    required: true,
 | 
			
		||||
    encrypt: true,
 | 
			
		||||
  })
 | 
			
		||||
  accessKeySecret = '';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @AccessInput({
 | 
			
		||||
    title: "测试",
 | 
			
		||||
    component: {
 | 
			
		||||
      name: "api-test",
 | 
			
		||||
      action: "TestRequest"
 | 
			
		||||
    },
 | 
			
		||||
    helper: "点击测试接口是否正常"
 | 
			
		||||
  })
 | 
			
		||||
  testRequest = true;
 | 
			
		||||
 | 
			
		||||
  async onTestRequest() {
 | 
			
		||||
    const client = await this.getCdnClient()
 | 
			
		||||
    await this.getCertList({client})
 | 
			
		||||
    return "ok"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  async getCertList(opts?:{client:KsyunClient,pageNo?:number;pageSize?:number}) {
 | 
			
		||||
    const res = await opts.client.doRequest({
 | 
			
		||||
      action: "GetCertificates",
 | 
			
		||||
      version: "2016-09-01",
 | 
			
		||||
      method:"POST",
 | 
			
		||||
      url:"/2016-09-01/cert/GetCertificates",
 | 
			
		||||
      data:{
 | 
			
		||||
        PageNum:opts?.pageNo || 1,
 | 
			
		||||
        PageSize: opts?.pageSize || 30
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    this.ctx.logger.info(res)
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * CertificateId	是	string	证书对应的唯一ID
 | 
			
		||||
   * CertificateName	是	String	安全证书名称
 | 
			
		||||
   * ServerCertificate	是	String	域名对应的安全证书内容
 | 
			
		||||
   * PrivateKey
 | 
			
		||||
   * @param opts
 | 
			
		||||
   */
 | 
			
		||||
  async updateCert(opts:{
 | 
			
		||||
      client:KsyunClient,
 | 
			
		||||
      certId:string,
 | 
			
		||||
      certName:string,
 | 
			
		||||
      cert:CertInfo
 | 
			
		||||
  }){
 | 
			
		||||
    const res = await opts.client.doRequest({
 | 
			
		||||
      action: "SetCertificate",
 | 
			
		||||
      version: "2016-09-01",
 | 
			
		||||
      method:"POST",
 | 
			
		||||
      url:"/2016-09-01/cert/SetCertificate",
 | 
			
		||||
      data:{
 | 
			
		||||
        CertificateId: parseInt(opts.certId),
 | 
			
		||||
        CertificateName: opts.certName,
 | 
			
		||||
        ServerCertificate: opts.cert.crt,
 | 
			
		||||
        PrivateKey: opts.cert.key
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    this.ctx.logger.info(res)
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getCdnClient() {
 | 
			
		||||
    return new KsyunClient({
 | 
			
		||||
      accessKeyId: this.accessKeyId,
 | 
			
		||||
      secretAccessKey: this.accessKeySecret,
 | 
			
		||||
      region: 'cn-beijing-6',
 | 
			
		||||
      service: 'cdn',
 | 
			
		||||
      endpoint: 'cdn.api.ksyun.com',
 | 
			
		||||
      logger: this.ctx.logger,
 | 
			
		||||
      http: this.ctx.http
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
new KsyunAccess();
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,351 @@
 | 
			
		|||
import crypto from 'crypto';
 | 
			
		||||
import querystring from 'querystring'
 | 
			
		||||
import {HttpClient, HttpRequestConfig, ILogger} from "@certd/basic";
 | 
			
		||||
 | 
			
		||||
export class KsyunClient {
 | 
			
		||||
 | 
			
		||||
  accessKeyId: string;
 | 
			
		||||
  secretAccessKey: string;
 | 
			
		||||
  region: string;
 | 
			
		||||
  service: string;
 | 
			
		||||
  endpoint: string;
 | 
			
		||||
  logger: ILogger;
 | 
			
		||||
  http: HttpClient
 | 
			
		||||
  constructor(opts:{accessKeyId:string; secretAccessKey:string; region?:string; service :string;endpoint :string,logger:ILogger,http:HttpClient}) {
 | 
			
		||||
    this.accessKeyId = opts.accessKeyId;
 | 
			
		||||
    this.secretAccessKey = opts.secretAccessKey;
 | 
			
		||||
    this.region = opts.region || 'cn-beijing-6';
 | 
			
		||||
    this.service = opts.service;
 | 
			
		||||
    this.endpoint =opts.endpoint
 | 
			
		||||
    this.logger = opts.logger;
 | 
			
		||||
    this.http = opts.http;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 async doRequest(opts: {action:string;version:string} &HttpRequestConfig){
 | 
			
		||||
    const config =  this.signRequest({
 | 
			
		||||
      method: opts.method || 'GET',
 | 
			
		||||
      url: opts.url || '/2016-09-01/domain/GetCdnDomains',
 | 
			
		||||
      baseURL: `https://${this.endpoint}`,
 | 
			
		||||
      params: opts.params,
 | 
			
		||||
      headers: {
 | 
			
		||||
        'X-Action': opts.action,
 | 
			
		||||
        'X-Version': opts.version
 | 
			
		||||
      },
 | 
			
		||||
      data: opts.data
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    try{
 | 
			
		||||
      return await this.http.request(config)
 | 
			
		||||
    }catch (e) {
 | 
			
		||||
      if (e.response?.data?.Error?.Message){
 | 
			
		||||
        throw new Error(e.response?.data?.Error?.Message)
 | 
			
		||||
      }
 | 
			
		||||
      throw e
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 签名请求
 | 
			
		||||
   * @param {Object} config Axios 请求配置
 | 
			
		||||
   * @returns {Object} 签名后的请求配置
 | 
			
		||||
   */
 | 
			
		||||
  signRequest(config) {
 | 
			
		||||
    // 确保有必要的配置
 | 
			
		||||
    if (!this.accessKeyId || !this.secretAccessKey) {
 | 
			
		||||
      throw new Error('AccessKeyId and SecretAccessKey are required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 设置默认值
 | 
			
		||||
    config.method = config.method || 'GET';
 | 
			
		||||
    config.headers = config.headers || {};
 | 
			
		||||
 | 
			
		||||
    // 获取当前时间并设置 X-Amz-Date
 | 
			
		||||
    const requestDate = this.getRequestDate();
 | 
			
		||||
    config.headers['x-amz-date'] = requestDate;
 | 
			
		||||
 | 
			
		||||
    // 处理不同的请求方法
 | 
			
		||||
    let canonicalQueryString = '';
 | 
			
		||||
    let hashedPayload = this.hashPayload(config.data || '');
 | 
			
		||||
 | 
			
		||||
    if (config.method.toUpperCase() === 'GET') {
 | 
			
		||||
      // GET 请求 - 参数在 URL 中
 | 
			
		||||
      const urlParts = config.url.split('?');
 | 
			
		||||
      const path = urlParts[0];
 | 
			
		||||
      const query = urlParts[1] || '';
 | 
			
		||||
 | 
			
		||||
      // 合并现有查询参数和额外参数
 | 
			
		||||
      const queryParams = {
 | 
			
		||||
        ...querystring.parse(query),
 | 
			
		||||
        ...(config.params || {})
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // 生成规范查询字符串
 | 
			
		||||
      canonicalQueryString = this.createCanonicalQueryString(queryParams);
 | 
			
		||||
      config.url = `${path}?${canonicalQueryString}`;
 | 
			
		||||
      config.params = {}; // 清空 params,因为已经合并到 URL 中
 | 
			
		||||
    } else {
 | 
			
		||||
      // POST/PUT 等请求 - 参数在 body 中
 | 
			
		||||
      canonicalQueryString = '';
 | 
			
		||||
      if (config.data && typeof config.data === 'object') {
 | 
			
		||||
        // 如果 data 是对象,转换为 JSON 字符串
 | 
			
		||||
        config.data = JSON.stringify(config.data);
 | 
			
		||||
        hashedPayload = this.hashPayload(config.data);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 生成规范请求
 | 
			
		||||
    const canonicalRequest = this.createCanonicalRequest(
 | 
			
		||||
      config.method,
 | 
			
		||||
      config.url,
 | 
			
		||||
      canonicalQueryString,
 | 
			
		||||
      config.headers,
 | 
			
		||||
      hashedPayload
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 生成签名字符串
 | 
			
		||||
    const credentialScope = this.createCredentialScope(requestDate);
 | 
			
		||||
    const stringToSign = this.createStringToSign(requestDate, credentialScope, canonicalRequest);
 | 
			
		||||
 | 
			
		||||
    // 计算签名
 | 
			
		||||
    const signature = this.calculateSignature(requestDate, stringToSign);
 | 
			
		||||
 | 
			
		||||
    // 生成 Authorization 头
 | 
			
		||||
    const signedHeaders = this.getSignedHeaders(config.headers);
 | 
			
		||||
    const authorizationHeader = this.createAuthorizationHeader(
 | 
			
		||||
      credentialScope,
 | 
			
		||||
      signedHeaders,
 | 
			
		||||
      signature
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 添加 Authorization 头
 | 
			
		||||
    config.headers.Authorization = authorizationHeader;
 | 
			
		||||
 | 
			
		||||
    return config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取当前时间 (格式: YYYYMMDD'T'HHMMSS'Z')
 | 
			
		||||
   * @returns {string} 格式化后的时间字符串
 | 
			
		||||
   */
 | 
			
		||||
  getRequestDate() {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const year = now.getUTCFullYear();
 | 
			
		||||
    const month = String(now.getUTCMonth() + 1).padStart(2, '0');
 | 
			
		||||
    const day = String(now.getUTCDate()).padStart(2, '0');
 | 
			
		||||
    const hours = String(now.getUTCHours()).padStart(2, '0');
 | 
			
		||||
    const minutes = String(now.getUTCMinutes()).padStart(2, '0');
 | 
			
		||||
    const seconds = String(now.getUTCSeconds()).padStart(2, '0');
 | 
			
		||||
 | 
			
		||||
    return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 哈希 payload
 | 
			
		||||
   * @param {string} payload 请求体内容
 | 
			
		||||
   * @returns {string} 哈希后的16进制字符串
 | 
			
		||||
   */
 | 
			
		||||
  hashPayload(payload) {
 | 
			
		||||
    if (typeof payload !== 'string') {
 | 
			
		||||
      payload = '';
 | 
			
		||||
    }
 | 
			
		||||
    return crypto.createHash('sha256').update(payload).digest('hex').toLowerCase();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建规范查询字符串
 | 
			
		||||
   * @param {Object} params 查询参数对象
 | 
			
		||||
   * @returns {string} 规范化的查询字符串
 | 
			
		||||
   */
 | 
			
		||||
  createCanonicalQueryString(params) {
 | 
			
		||||
    // 对参数名和值进行 URI 编码
 | 
			
		||||
    const encodedParams = {};
 | 
			
		||||
    for (const key in params) {
 | 
			
		||||
      if (params.hasOwnProperty(key)) {
 | 
			
		||||
        const encodedKey = this.uriEncode(key);
 | 
			
		||||
        const encodedValue = this.uriEncode(params[key].toString());
 | 
			
		||||
        encodedParams[encodedKey] = encodedValue;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 按 ASCII 顺序排序
 | 
			
		||||
    const sortedKeys = Object.keys(encodedParams).sort();
 | 
			
		||||
 | 
			
		||||
    // 构建查询字符串
 | 
			
		||||
    return sortedKeys.map(key => `${key}=${encodedParams[key]}`).join('&');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * URI 编码 (符合 AWS 规范)
 | 
			
		||||
   * @param {string} str 要编码的字符串
 | 
			
		||||
   * @returns {string} 编码后的字符串
 | 
			
		||||
   */
 | 
			
		||||
  uriEncode(str) {
 | 
			
		||||
    return encodeURIComponent(str)
 | 
			
		||||
      .replace(/[^A-Za-z0-9\-_.~]/g, c =>
 | 
			
		||||
        '%' + c.charCodeAt(0).toString(16).toUpperCase());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建规范请求
 | 
			
		||||
   * @param {string} method HTTP 方法
 | 
			
		||||
   * @param {string} url 请求 URL
 | 
			
		||||
   * @param {string} queryString 查询字符串
 | 
			
		||||
   * @param {Object} headers 请求头
 | 
			
		||||
   * @param {string} hashedPayload 哈希后的 payload
 | 
			
		||||
   * @returns {string} 规范化的请求字符串
 | 
			
		||||
   */
 | 
			
		||||
  createCanonicalRequest(method, url, queryString, headers, hashedPayload) {
 | 
			
		||||
    // 获取规范 URI
 | 
			
		||||
    const urlObj = new URL(url, 'http://dummy.com'); // 使用虚拟基础 URL 来解析路径
 | 
			
		||||
    const canonicalUri = this.uriEncodePath(urlObj.pathname) || '/';
 | 
			
		||||
 | 
			
		||||
    // 获取规范 headers 和 signed headers
 | 
			
		||||
    const { canonicalHeaders, signedHeaders } = this.createCanonicalHeaders(headers);
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
      method.toUpperCase(),
 | 
			
		||||
      canonicalUri,
 | 
			
		||||
      queryString,
 | 
			
		||||
      canonicalHeaders,
 | 
			
		||||
      signedHeaders,
 | 
			
		||||
      hashedPayload
 | 
			
		||||
    ].join('\n');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * URI 编码路径部分
 | 
			
		||||
   * @param {string} path 路径
 | 
			
		||||
   * @returns {string} 编码后的路径
 | 
			
		||||
   */
 | 
			
		||||
  uriEncodePath(path) {
 | 
			
		||||
    // 分割路径为各个部分,分别编码
 | 
			
		||||
    return path.split('/').map(part => this.uriEncode(part)).join('/');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建规范 headers 和 signed headers
 | 
			
		||||
   * @param {Object} headers 原始请求头
 | 
			
		||||
   * @returns {Object} { canonicalHeaders: string, signedHeaders: string }
 | 
			
		||||
   */
 | 
			
		||||
  createCanonicalHeaders(headers) {
 | 
			
		||||
    // 处理 headers
 | 
			
		||||
    const headerMap:any = {};
 | 
			
		||||
 | 
			
		||||
    // 标准化 headers
 | 
			
		||||
    for (const key in headers) {
 | 
			
		||||
      if (headers.hasOwnProperty(key)) {
 | 
			
		||||
        const lowerKey = key.toLowerCase();
 | 
			
		||||
        let value = headers[key]
 | 
			
		||||
        if (value) {
 | 
			
		||||
         value = value.toString().replace(/\s+/g, ' ').trim();
 | 
			
		||||
          headerMap[lowerKey] = value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 确保 host 和 x-amz-date 存在
 | 
			
		||||
    if (!headerMap.host) {
 | 
			
		||||
      const url = headers.host ||this.endpoint || 'cdn.api.ksyun.com'; // 默认值
 | 
			
		||||
      headerMap.host = url.replace(/^https?:\/\//, '').split('/')[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 按 header 名称排序
 | 
			
		||||
    const sortedHeaderNames = Object.keys(headerMap).sort();
 | 
			
		||||
 | 
			
		||||
    // 构建规范 headers
 | 
			
		||||
    let canonicalHeaders = '';
 | 
			
		||||
    for (const name of sortedHeaderNames) {
 | 
			
		||||
      canonicalHeaders += `${name}:${headerMap[name]}\n`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 构建 signed headers
 | 
			
		||||
    const signedHeaders = sortedHeaderNames.join(';');
 | 
			
		||||
 | 
			
		||||
    return { canonicalHeaders, signedHeaders };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取 signed headers
 | 
			
		||||
   * @param {Object} headers 请求头
 | 
			
		||||
   * @returns {string} signed headers 字符串
 | 
			
		||||
   */
 | 
			
		||||
  getSignedHeaders(headers) {
 | 
			
		||||
    const { signedHeaders } = this.createCanonicalHeaders(headers);
 | 
			
		||||
    return signedHeaders;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建信任状范围
 | 
			
		||||
   * @param {string} requestDate 请求日期 (YYYYMMDDTHHMMSSZ)
 | 
			
		||||
   * @returns {string} 信任状范围字符串
 | 
			
		||||
   */
 | 
			
		||||
  createCredentialScope(requestDate) {
 | 
			
		||||
    const date = requestDate.split('T')[0];
 | 
			
		||||
    return `${date}/${this.region}/${this.service}/aws4_request`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建签名字符串
 | 
			
		||||
   * @param {string} requestDate 请求日期
 | 
			
		||||
   * @param {string} credentialScope 信任状范围
 | 
			
		||||
   * @param {string} canonicalRequest 规范请求
 | 
			
		||||
   * @returns {string} 签名字符串
 | 
			
		||||
   */
 | 
			
		||||
  createStringToSign(requestDate, credentialScope, canonicalRequest) {
 | 
			
		||||
    const algorithm = 'AWS4-HMAC-SHA256';
 | 
			
		||||
    const hashedCanonicalRequest = crypto.createHash('sha256')
 | 
			
		||||
      .update(canonicalRequest)
 | 
			
		||||
      .digest('hex')
 | 
			
		||||
      .toLowerCase();
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
      algorithm,
 | 
			
		||||
      requestDate,
 | 
			
		||||
      credentialScope,
 | 
			
		||||
      hashedCanonicalRequest
 | 
			
		||||
    ].join('\n');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 计算签名
 | 
			
		||||
   * @param {string} requestDate 请求日期
 | 
			
		||||
   * @param {string} stringToSign 签名字符串
 | 
			
		||||
   * @returns {string} 签名值
 | 
			
		||||
   */
 | 
			
		||||
  calculateSignature(requestDate, stringToSign) {
 | 
			
		||||
    const date = requestDate.split('T')[0];
 | 
			
		||||
    const kDate = this.hmac(`AWS4${this.secretAccessKey}`, date);
 | 
			
		||||
    const kRegion = this.hmac(kDate, this.region);
 | 
			
		||||
    const kService = this.hmac(kRegion, this.service);
 | 
			
		||||
    const kSigning = this.hmac(kService, 'aws4_request');
 | 
			
		||||
 | 
			
		||||
    return this.hmac(kSigning, stringToSign, 'hex');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * HMAC-SHA256 计算
 | 
			
		||||
   * @param {string|Buffer} key 密钥
 | 
			
		||||
   * @param {string} data 数据
 | 
			
		||||
   * @param {string} [encoding] 输出编码
 | 
			
		||||
   * @returns {string|Buffer} HMAC 结果
 | 
			
		||||
   */
 | 
			
		||||
  hmac(key, data, encoding = null) {
 | 
			
		||||
    const hmac = crypto.createHmac('sha256', key);
 | 
			
		||||
    hmac.update(data);
 | 
			
		||||
    return encoding ? hmac.digest(encoding) : hmac.digest();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 创建 Authorization 头
 | 
			
		||||
   * @param {string} credentialScope 信任状范围
 | 
			
		||||
   * @param {string} signedHeaders signed headers
 | 
			
		||||
   * @param {string} signature 签名值
 | 
			
		||||
   * @returns {string} Authorization 头值
 | 
			
		||||
   */
 | 
			
		||||
  createAuthorizationHeader(credentialScope, signedHeaders, signature) {
 | 
			
		||||
    return `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
export * from "./plugins/index.js";
 | 
			
		||||
export * from "./access.js";
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
import "./plugin-refresh-cert.js"
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
import {
 | 
			
		||||
  AbstractTaskPlugin,
 | 
			
		||||
  IsTaskPlugin,
 | 
			
		||||
  Pager,
 | 
			
		||||
  PageSearch,
 | 
			
		||||
  pluginGroups,
 | 
			
		||||
  RunStrategy,
 | 
			
		||||
  TaskInput
 | 
			
		||||
} from "@certd/pipeline";
 | 
			
		||||
import {CertApplyPluginNames, CertInfo, CertReader} from "@certd/plugin-cert";
 | 
			
		||||
import {createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
 | 
			
		||||
import {KsyunAccess} from "../access.js";
 | 
			
		||||
 | 
			
		||||
@IsTaskPlugin({
 | 
			
		||||
  //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
 | 
			
		||||
  name: "KsyunRefreshCert",
 | 
			
		||||
  title: "金山云-更新CDN证书",
 | 
			
		||||
  desc: "金山云自动更新CDN证书",
 | 
			
		||||
  icon: "svg:icon-lucky",
 | 
			
		||||
  //插件分组
 | 
			
		||||
  group: pluginGroups.cdn.key,
 | 
			
		||||
  needPlus: false,
 | 
			
		||||
  default: {
 | 
			
		||||
    //默认值配置照抄即可
 | 
			
		||||
    strategy: {
 | 
			
		||||
      runStrategy: RunStrategy.SkipWhenSucceed
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
//类名规范,跟上面插件名称(name)一致
 | 
			
		||||
export class KsyunRefreshCDNCert extends AbstractTaskPlugin {
 | 
			
		||||
  //证书选择,此项必须要有
 | 
			
		||||
  @TaskInput({
 | 
			
		||||
    title: "域名证书",
 | 
			
		||||
    helper: "请选择前置任务输出的域名证书",
 | 
			
		||||
    component: {
 | 
			
		||||
      name: "output-selector",
 | 
			
		||||
      from: [...CertApplyPluginNames]
 | 
			
		||||
    }
 | 
			
		||||
    // required: true, // 必填
 | 
			
		||||
  })
 | 
			
		||||
  cert!: CertInfo;
 | 
			
		||||
 | 
			
		||||
  @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
 | 
			
		||||
  certDomains!: string[];
 | 
			
		||||
 | 
			
		||||
  //授权选择框
 | 
			
		||||
  @TaskInput({
 | 
			
		||||
    title: "金山云授权",
 | 
			
		||||
    component: {
 | 
			
		||||
      name: "access-selector",
 | 
			
		||||
      type: "ksyun" //固定授权类型
 | 
			
		||||
    },
 | 
			
		||||
    required: true //必填
 | 
			
		||||
  })
 | 
			
		||||
  accessId!: string;
 | 
			
		||||
  //
 | 
			
		||||
 | 
			
		||||
  @TaskInput(
 | 
			
		||||
    createRemoteSelectInputDefine({
 | 
			
		||||
      title: "证书Id",
 | 
			
		||||
      helper: "要更新的金山云CDN证书id,如果这里没有,请先给cdn域名手动绑定一次证书",
 | 
			
		||||
      action: KsyunRefreshCDNCert.prototype.onGetCertList.name,
 | 
			
		||||
      pager: false,
 | 
			
		||||
      search: false
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
  certList!: string[];
 | 
			
		||||
 | 
			
		||||
  //插件实例化时执行的方法
 | 
			
		||||
  async onInstance() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  //插件执行方法
 | 
			
		||||
  async execute(): Promise<void> {
 | 
			
		||||
    const access = await this.getAccess<KsyunAccess>(this.accessId);
 | 
			
		||||
 | 
			
		||||
    const certReader = new CertReader(this.cert)
 | 
			
		||||
    const certName = certReader.buildCertName()
 | 
			
		||||
    const client = await access.getCdnClient();
 | 
			
		||||
    for (const item of this.certList) {
 | 
			
		||||
      this.logger.info(`----------- 开始更新证书:${item}`);
 | 
			
		||||
      await access.updateCert({
 | 
			
		||||
        client,
 | 
			
		||||
        certId: item,
 | 
			
		||||
        certName,
 | 
			
		||||
        cert: this.cert
 | 
			
		||||
      });
 | 
			
		||||
      this.logger.info(`----------- 更新证书${item}成功`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.logger.info("部署完成");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async onGetCertList(data: PageSearch = {}) {
 | 
			
		||||
    const access = await this.getAccess<KsyunAccess>(this.accessId);
 | 
			
		||||
 | 
			
		||||
    const client = await access.getCdnClient();
 | 
			
		||||
    const pager = new Pager(data)
 | 
			
		||||
    const res = await access.getCertList({client,
 | 
			
		||||
      pageNo: pager.pageNo ,
 | 
			
		||||
      pageSize: pager.pageSize
 | 
			
		||||
    })
 | 
			
		||||
    const list = res.Certificates
 | 
			
		||||
    if (!list || list.length === 0) {
 | 
			
		||||
      throw new Error("没有找到证书,请先在控制台手动上传一次证书");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const total  = res.TotalCount
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * certificate-id
 | 
			
		||||
     * name
 | 
			
		||||
     * dns-names
 | 
			
		||||
     */
 | 
			
		||||
    const options = list.map((item: any) => {
 | 
			
		||||
      return {
 | 
			
		||||
        label: `${item.CertificateName}<${item.CertificateId}-${item.ConfigDomainNames}>`,
 | 
			
		||||
        value: item.CertificateId,
 | 
			
		||||
        domain: item.ConfigDomainNames
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
    return {
 | 
			
		||||
      list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
 | 
			
		||||
      total: total,
 | 
			
		||||
      pageNo: pager.pageNo,
 | 
			
		||||
      pageSize: pager.pageSize
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//实例化一下,注册插件
 | 
			
		||||
new KsyunRefreshCDNCert();
 | 
			
		||||
		Loading…
	
		Reference in New Issue