perf: 支持部署到中国移动CDN

This commit is contained in:
xiaojunnuo
2025-12-04 00:46:25 +08:00
parent a5ca41131b
commit 43513049be
5 changed files with 600 additions and 0 deletions

View File

@@ -39,3 +39,4 @@ export * from './plugin-captcha/index.js'
export * from './plugin-xinnet/index.js'
export * from './plugin-xinnetconnet/index.js'
export * from './plugin-oauth/index.js'
export * from './plugin-cmcc/index.js'

View File

@@ -0,0 +1,66 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import { CmccClient } from "./cmcc-client.js";
/**
*
* tenantId: string;
tenantKey: string;
endpoint?: string;
*/
@IsAccess({
name: "cmcc",
title: "中国移动CND授权",
desc: "",
icon: "clarity:plugin-line"
})
export class CmccAccess extends BaseAccess {
@AccessInput({
title: 'TenantID',
component: {
placeholder: 'TenantID',
},
required: true,
})
tenantId = '';
@AccessInput({
title: 'TenantKey',
component: {
placeholder: 'TenantKey',
},
required: true,
encrypt: true,
})
tenantKey = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
const client = await this.getCmccClient()
await client.getDomainList({})
return "ok"
}
async getCmccClient() {
return new CmccClient({
tenantId: this.tenantId,
tenantKey: this.tenantKey,
http: this.ctx.http,
logger: this.ctx.logger,
})
}
}
new CmccAccess();

View File

@@ -0,0 +1,405 @@
import { HttpClient, ILogger } from '@certd/basic';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import * as crypto from 'crypto';
export interface CmcdnConfig {
tenantId: string;
tenantKey: string;
endpoint?: string;
http: HttpClient;
logger: ILogger;
}
/**
* 移动CDN平台SDK
*/
export class CmccClient {
private config: Required<CmcdnConfig>;
private token: string | null = null;
private tokenExpiresAt: number | null = null;
private http: HttpClient;
private logger: ILogger;
/**
* 构造函数
* @param config 配置信息
*/
constructor(config: CmcdnConfig) {
this.config = {
endpoint: 'https://p.cdn.10086.cn/',
...config,
};
this.http = config.http
this.logger = config.logger;
if (!this.config.tenantId) {
throw new Error('tenantId is required');
}
if (!this.config.tenantKey) {
throw new Error('tenantKey is required');
}
}
/**
* 生成SHA256哈希
* @param data 输入数据
* @returns SHA256哈希值
*/
private sha256Hex(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* 获取当前ISO8601格式时间
* @returns ISO8601时间字符串
*/
private getCurrentIsoTime(): string {
return new Date().toISOString();
}
/**
* 生成认证请求签名
* @param datetime 请求时间
* @returns 签名
*/
private generateAuthSign(datetime: string): string {
const signData = `${this.config.tenantId}${datetime}${this.config.tenantKey}`;
return this.sha256Hex(signData);
}
/**
* 生成API请求签名
* @param body 请求体
* @param token 认证token
* @returns 签名
*/
private generateApiSign(body: any, token: string): string {
const bodyStr = body ? JSON.stringify(body) : '';
return this.sha256Hex(bodyStr + token);
}
/**
* 检查token是否有效
* @returns token是否有效
*/
private isTokenValid(): boolean {
if (!this.token || !this.tokenExpiresAt) {
return false;
}
return Date.now() < this.tokenExpiresAt;
}
/**
* 获取认证token
* @returns 认证token
*/
async getToken(): Promise<string> {
// 检查是否有有效的token
if (this.isTokenValid()) {
return this.token!;
}
const datetime = this.getCurrentIsoTime();
const sign = this.generateAuthSign(datetime);
const authRequest = {
datetime,
authorization: {
tenant_id: this.config.tenantId,
sign,
},
};
const response = await this.http.request({
baseURL: this.config.endpoint,
url: '/api/authentication',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
data: authRequest,
skipSslVerify: true,
logParams: false,
logRes: false,
logData: false
});
this.token = response.token;
// Token有效期为12小时
this.tokenExpiresAt = Date.now() + 12 * 60 * 60 * 1000;
return this.token;
}
/**
* 调用API
* @param req 请求选项
* @returns API响应
*/
async doRequest(req: any): Promise<any> {
// 获取有效的token
const token = await this.getToken();
// 设置默认headers
const defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/vnd.cmcdn+json',
'CMCDN-Auth-Token': token,
};
// 生成签名
if (req.method === 'POST' || req.method === 'PUT') {
const signature = this.generateApiSign(req.data, token);
defaultHeaders['HTTP-X-CMCDN-Signature'] = signature;
} else {
const signature = this.sha256Hex(token);
defaultHeaders['HTTP-X-CMCDN-Signature'] = signature;
}
// 合并自定义headers
const headers = { ...defaultHeaders, ...req.headers };
// 发送请求
try {
const response = await this.http.request({
baseURL: this.config.endpoint,
url: req.url,
method: req.method,
headers: headers,
data: req.data,
skipSslVerify: true,
logParams: false,
logRes: false,
logData: false
});
if (response.error_code != 0) {
this.logger.error(`接口请求失败,${JSON.stringify(response)}`);
throw new Error(response.error_msg || "接口请求失败");
}
return response.data;
} catch (error) {
this.logger.error(`接口请求失败,${error.response?.data?.error_msg || error.message}`);
throw new Error(error.response?.data?.error_msg || error.message);
}
}
/**
* 清除token
*/
clearToken(): void {
this.token = null;
this.tokenExpiresAt = null;
}
/**
* 获取当前token
* @returns 当前token
*/
getCurrentToken(): string | null {
return this.token;
}
/**
*
域名列表查询
本接口由 CDN 运营平台提供 ,所有外部 EC 客户使用。该接口为客户提供该客户各状态域名列表查询。本接口仅支持 JSON 结构。
7.1 目录信息
/api/domain_list?domainName =${domainName}&domainStatus =${domainStatus}
7.2 请求方法
GET
7.3 响应状态码
请求接收成功 201 body 内容详见下一节;
授权错误 403
请求错误 400
其他 :见 1.2.5 状态码。
7.4 JSON 结构规范
7.4.1 请求 URI 参数
序号 父元素 元素名称 约束 类型 长度 描述
1 domainName ? String 域名模糊匹配过滤
2 domainStatus ? String 域名状态过滤online启用
offline停用
configuring配置中
configure_failed :配置失败
7.4.2 请求 URI 示例
GET http://xxx.com/api/domain_list?domainName=www.test.com&domainStatus=online
7.4.3 响应数据体
序号 父元 元素名称 约束 类型 长度 描述
1 data 1 array 查询结果
2 data domainName 1 String 加速域名名称
3 data createTime 1 datetime 加速域名创建时间(2017-07-25 17:45:52)
4 data cname * String 加速域名对应的 CNAME 域名
5 data type * String 域名产品类型:
demand:点播产品(视音频/网页、下载);
live:直播产品
6 data status * String 域名状态:启用
停用
配置中
配置失败待分发
已生效启用中
删除中
7.4.4 响应报文示例
{
"data": [{
"createTime": "2017-07-25 17:45:52",
"domainName": "www.ponshine.com",
"cname": "www.ponshine.com.cmcdn.cdn.10086.cn", "type ": "demand"
},
{
"createTime": "2018-11-07 22:09:41",
"domainName": "www.testcustom.com",
"cname": "www.testcustom.com.cmcdn.cdn.10086.cn", "type ": "live"
}
]
}
*/
async getDomainList(req: { domainName?: string, domainStatus?: string }) {
const res = await this.doRequest({
url: "/api/domain_list",
method: "GET",
params: {
domainName: req.domainName,
domainStatus: req.domainStatus,
}
})
this.logger.info("getDomainList", res);
return res.data;
}
/**
* /api/config/action?commandType =saveCrt&version =1
12.1.2 请求方法
新增 POST
修改 PUT
12.1.3 响应状态码
请求接收成功: 200/201 body内容详见下一节
授权错误 403
请求错误 400
其他 见1.2.5状态码。
12.1.4 JSON 结构规范
12.1.4.1请求数据体
参数 说明 类型 约束
certificate 证书 ,仅支持 PEM 格式,
证书内的换行符使用字符串“\n”代替
内容如需加密传输可使用
PBEWith MD5And DES 加密 ,秘钥将私下
提供
string
必选
private_key 私钥 ,仅支持 PEM 格式,
私钥内的换行符使用字符串“\n”代替
内容如需加密传输可使用
PBEWith MD5And DES 加密 ,秘钥将私下
提供
string
必选
crt_name 证书名称 ,不支持修改 ,有传 unique_id
时不需要
(仅支持英文字母、数字、下划线 ,最大长
string
必选
度为 32 个字符)
unique_id 证书唯一 id ,修改证书时该项必选;
修改证书时 ,如历史证书已绑定域名,
修改后证书也需支持对应域名
string
修改必选
contact_name 证书联系人 string 可选
contact_mobile 证书联系人手机号 string 可选
contact_email 证书联系人邮箱 string 可选
12.1.4.2请求报文示例
*/
async uploadCert(req: { cert: CertInfo }) {
const certReader = new CertReader(req.cert);
const res = await this.doRequest({
url: "/api/config/action?commandType=saveCrt&version=1",
method: "POST",
data: {
certificate: req.cert.crt,
private_key: req.cert.key,
crt_name: certReader.buildCertName(),
}
})
this.logger.info("uploadCert", res);
return res;
}
/**
*
* @param req
*/
async deployCertToCdn(req: { domainNames: string[], certId: string }) {
// /api/config/action?commandType = manageDomainBaseConfig&version = 1
const res = await this.doRequest({
url: "/api/config/action?commandType=manageDomainBaseConfig&version=1",
method: "PUT",
data: {
modify_type: 0,
domains: req.domainNames,
https_enable: true,
unique_id: req.certId,
}
})
this.logger.info("deployCertToCdn", res);
return res.data;
}
}

View File

@@ -0,0 +1,2 @@
export * from './access.js'
export * from './plugin-deploy-to-cdn.js'

View File

@@ -0,0 +1,126 @@
import {
IsTaskPlugin,
PageSearch,
pluginGroups,
RunStrategy,
TaskInput
} from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { AbstractPlusTaskPlugin } from "@certd/plugin-plus";
import { CmccAccess } from "./access.js";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
name: "CmccDeployCertToCdn",
title: "中国移动-部署证书到CDN",
desc: "中国移动自动部署证书到CDN",
icon: "svg:icon-lucky",
//插件分组
group: pluginGroups.cdn.key,
needPlus: true,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
//类名规范跟上面插件名称name一致
export class CmccDeployCertToCdn extends AbstractPlusTaskPlugin {
//证书选择,此项必须要有
@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: "cmcc" //固定授权类型
},
required: true //必填
})
accessId!: string;
//
@TaskInput(
createRemoteSelectInputDefine({
title: "加速域名",
helper: "要更新的中国移动CDN域名",
action: CmccDeployCertToCdn.prototype.onGetDomainList.name,
pager: false,
search: false
})
)
domainList!: string[];
//插件实例化时执行的方法
async onInstance() {
}
//插件执行方法
async execute(): Promise<void> {
const access = await this.getAccess<CmccAccess>(this.accessId);
const client = await access.getCmccClient();
this.logger.info(`----------- 开始更新证书:${this.domainList}`);
const newCert = await client.uploadCert({
cert: this.cert
})
const certId = newCert.unique_id
this.logger.info(`----------- 上传证书成功,证书ID:${certId}`);
await client.deployCertToCdn({
certId: certId,
domainNames: this.domainList
});
this.logger.info(`----------- 更新证书${this.domainList}成功,等待10s`);
await this.ctx.utils.sleep(10000);
this.logger.info("部署完成");
}
async onGetDomainList(data: PageSearch = {}) {
const access = await this.getAccess<CmccAccess>(this.accessId);
const client= await access.getCmccClient();
const res = await client.getDomainList({})
const list = res || []
if (!list || list.length === 0) {
throw new Error("没有找到加速域名");
}
/**
* certificate-id
* name
* dns-names
*/
const options = list.map((item: any) => {
return {
label: `${item.domainName}`,
value: item.domainName,
domain: item.domainName
};
});
return {
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
};
}
}
//实例化一下,注册插件
new CmccDeployCertToCdn();