diff --git a/packages/core/basic/src/utils/util.request.ts b/packages/core/basic/src/utils/util.request.ts index 8a363e84..983b1e39 100644 --- a/packages/core/basic/src/utils/util.request.ts +++ b/packages/core/basic/src/utils/util.request.ts @@ -1,13 +1,13 @@ -import axios, { AxiosHeaders, AxiosRequestConfig } from 'axios'; -import { ILogger, logger } from './util.log.js'; -import { Logger } from 'log4js'; -import { HttpProxyAgent } from 'http-proxy-agent'; -import { HttpsProxyAgent } from 'https-proxy-agent'; -import nodeHttp from 'http'; -import * as https from 'node:https'; -import { merge } from 'lodash-es'; -import { safePromise } from './util.promise.js'; -import fs from 'fs'; +import axios, { AxiosHeaders, AxiosRequestConfig } from "axios"; +import { ILogger, logger } from "./util.log.js"; +import { Logger } from "log4js"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import nodeHttp from "http"; +import * as https from "node:https"; +import { merge } from "lodash-es"; +import { safePromise } from "./util.promise.js"; +import fs from "fs"; export class HttpError extends Error { status?: number; statusText?: string; @@ -22,10 +22,10 @@ export class HttpError extends Error { super(error.message || error.response?.statusText); const message = error?.message; - if (message && typeof message === 'string') { - if (message.indexOf && message.indexOf('ssl3_get_record:wrong version number') >= 0) { + if (message && typeof message === "string") { + if (message.indexOf && message.indexOf("ssl3_get_record:wrong version number") >= 0) { this.message = `${message}(http协议错误,服务端要求http协议,请检查是否使用了https请求)`; - } else if (message.indexOf('getaddrinfo EAI_AGAIN') >= 0) { + } else if (message.indexOf("getaddrinfo EAI_AGAIN") >= 0) { this.message = `${message}(无法解析域名,请检查网络连接或dns配置,更换docker-compose.yaml中dns配置)`; } } @@ -47,7 +47,7 @@ export class HttpError extends Error { }; let url = error.config?.url; if (error.config?.baseURL) { - url = (error.config?.baseURL || '') + url; + url = (error.config?.baseURL || "") + url; } if (url) { this.message = `${this.message} 【${url}】`; @@ -73,7 +73,7 @@ export const HttpCommonError = HttpError; let defaultAgents = createAgent(); export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) { - logger.info('setGlobalProxy:', opts); + logger.info("setGlobalProxy:", opts); defaultAgents = createAgent(opts); } @@ -102,12 +102,12 @@ export function createAxiosService({ logger }: { logger: Logger }) { if (config.skipSslVerify || config.httpProxy) { let rejectUnauthorized = true; if (config.skipSslVerify) { - logger.info('跳过SSL验证'); + logger.info("跳过SSL验证"); rejectUnauthorized = false; } const proxy: any = {}; if (config.httpProxy) { - logger.info('使用自定义http代理:', config.httpProxy); + logger.info("使用自定义http代理:", config.httpProxy); proxy.httpProxy = config.httpProxy; proxy.httpsProxy = config.httpProxy; } @@ -128,7 +128,7 @@ export function createAxiosService({ logger }: { logger: Logger }) { }, (error: Error) => { // 发送失败 - logger.error('接口请求失败:', error); + logger.error("接口请求失败:", error); return Promise.reject(error); } ); @@ -143,7 +143,7 @@ export function createAxiosService({ logger }: { logger: Logger }) { logger.info(`http response : status=${response?.status},data=${resData}`); } else { - logger.info('http response status:', response?.status); + logger.info("http response status:", response?.status); } if (response?.config?.returnResponse) { return response; @@ -154,53 +154,51 @@ export function createAxiosService({ logger }: { logger: Logger }) { const status = error.response?.status; switch (status) { case 400: - error.message = '请求错误'; + error.message = "请求错误"; break; case 401: - error.message = '认证/登录失败'; + error.message = "认证/登录失败"; break; case 403: - error.message = '拒绝访问'; + error.message = "拒绝访问"; break; case 404: error.message = `请求地址出错`; break; case 408: - error.message = '请求超时'; + error.message = "请求超时"; break; case 500: - error.message = '服务器内部错误'; + error.message = "服务器内部错误"; break; case 501: - error.message = '服务未实现'; + error.message = "服务未实现"; break; case 502: - error.message = '网关错误'; + error.message = "网关错误"; break; case 503: - error.message = '服务不可用'; + error.message = "服务不可用"; break; case 504: - error.message = '网关超时'; + error.message = "网关超时"; break; case 505: - error.message = 'HTTP版本不受支持'; + error.message = "HTTP版本不受支持"; break; default: break; } - logger.error( - `请求出错:status:${error.response?.status},statusText:${error.response?.statusText},url:${error.config?.url},method:${error.config?.method}。` - ); - logger.error('返回数据:', JSON.stringify(error.response?.data)); + logger.error(`请求出错:status:${error.response?.status},statusText:${error.response?.statusText},url:${error.config?.url},method:${error.config?.method}。`); + logger.error("返回数据:", JSON.stringify(error.response?.data)); if (error.response?.data) { const message = error.response.data.message || error.response.data.msg || error.response.data.error; - if (typeof message === 'string') { + if (typeof message === "string") { error.message = message; } } if (error instanceof AggregateError) { - logger.error('AggregateError', error); + logger.error("AggregateError", error); } const err = new HttpError(error); return Promise.reject(err); @@ -244,24 +242,24 @@ export function createAgent(opts: CreateAgentOptions = {}) { if (httpProxy) { process.env.HTTP_PROXY = httpProxy; process.env.http_proxy = httpProxy; - logger.info('use httpProxy:', httpProxy); + logger.info("use httpProxy:", httpProxy); httpAgent = new HttpProxyAgent(httpProxy, opts as any); merge(httpAgent.options, opts); } else { - process.env.HTTP_PROXY = ''; - process.env.http_proxy = ''; + process.env.HTTP_PROXY = ""; + process.env.http_proxy = ""; httpAgent = new nodeHttp.Agent(opts); } const httpsProxy = opts.httpsProxy; if (httpsProxy) { process.env.HTTPS_PROXY = httpsProxy; process.env.https_proxy = httpsProxy; - logger.info('use httpsProxy:', httpsProxy); + logger.info("use httpsProxy:", httpsProxy); httpsAgent = new HttpsProxyAgent(httpsProxy, opts as any); merge(httpsAgent.options, opts); } else { - process.env.HTTPS_PROXY = ''; - process.env.https_proxy = ''; + process.env.HTTPS_PROXY = ""; + process.env.https_proxy = ""; httpsAgent = new https.Agent(opts); } return { @@ -276,27 +274,27 @@ export async function download(req: { http: HttpClient; config: HttpRequestConfi http .request({ logRes: false, - responseType: 'stream', + responseType: "stream", ...config, }) .then(res => { const writer = fs.createWriteStream(savePath); res.pipe(writer); - writer.on('close', () => { - logger.info('文件下载成功'); + writer.on("close", () => { + logger.info("文件下载成功"); resolve(true); }); //error - writer.on('error', err => { - logger.error('下载失败', err); + writer.on("error", err => { + logger.error("下载失败", err); reject(err); }); //进度条打印 - const totalLength = res.headers['content-length']; + const totalLength = res.headers["content-length"]; let currentLength = 0; // 每5%打印一次 const step = (totalLength / 100) * 5; - res.on('data', (chunk: any) => { + res.on("data", (chunk: any) => { currentLength += chunk.length; if (currentLength % step < chunk.length) { const percent = ((currentLength / totalLength) * 100).toFixed(2); @@ -305,19 +303,19 @@ export async function download(req: { http: HttpClient; config: HttpRequestConfi }); }) .catch(err => { - logger.info('下载失败', err); + logger.info("下载失败", err); reject(err); }); }); } export function getCookie(response: any, name: string) { - const cookies = response.headers['set-cookie']; + const cookies = response.headers["set-cookie"]; //根据name 返回对应的cookie const found = cookies.find((cookie: any) => cookie.includes(name)); if (!found) { return null; } - const cookie = found.split(';')[0]; - return cookie.substring(cookie.indexOf('=') + 1); + const cookie = found.split(";")[0]; + return cookie.substring(cookie.indexOf("=") + 1); } diff --git a/packages/ui/certd-server/package.json b/packages/ui/certd-server/package.json index 3cbcb858..b53d43ca 100644 --- a/packages/ui/certd-server/package.json +++ b/packages/ui/certd-server/package.json @@ -76,6 +76,7 @@ "cos-nodejs-sdk-v5": "^2.14.6", "cron-parser": "^4.9.0", "cross-env": "^7.0.3", + "crypto-js": "^4.2.0", "dayjs": "^1.11.7", "form-data": "^4.0.0", "glob": "^11.0.0", diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index 587c33cd..594dd56e 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -18,3 +18,4 @@ export * from './plugin-dnsla/index.js'; export * from './plugin-upyun/index.js'; export * from './plugin-volcengine/index.js' export * from './plugin-jdcloud/index.js' +export * from './plugin-51dns/index.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-51dns/access.ts b/packages/ui/certd-server/src/plugins/plugin-51dns/access.ts new file mode 100644 index 00000000..b665483e --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-51dns/access.ts @@ -0,0 +1,40 @@ +import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline'; + +/** + * 这个注解将注册一个授权配置 + * 在certd的后台管理系统中,用户可以选择添加此类型的授权 + */ +@IsAccess({ + name: '51dns', + title: '51dns授权', + icon: 'arcticons:dns-changer-3', + desc: '', +}) +export class Dns51Access extends BaseAccess { + /** + * 授权属性配置 + */ + @AccessInput({ + title: '用户名', + component: { + placeholder: '用户名或手机号', + }, + required: true, + encrypt: false, + }) + username = ''; + + @AccessInput({ + title: '登录密码', + component: { + name:"a-input-password", + vModel:"value", + placeholder: '密码', + }, + required: true, + encrypt: true, + }) + password = ''; +} + +new Dns51Access(); diff --git a/packages/ui/certd-server/src/plugins/plugin-51dns/client.ts b/packages/ui/certd-server/src/plugins/plugin-51dns/client.ts new file mode 100644 index 00000000..4f094135 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-51dns/client.ts @@ -0,0 +1,200 @@ +import { createAxiosService, HttpClient, ILogger } from "@certd/basic"; +import { Dns51Access } from "./access.js"; + +export class Dns51Client { + logger: ILogger; + access: Dns51Access; + http: HttpClient; + cryptoJs: any; + isLogined = false; + _token = ""; + + constructor(options: { + logger: ILogger; + access: Dns51Access; + }) { + this.logger = options.logger; + this.access = options.access; + + this.http = createAxiosService({ + logger: this.logger + }); + + } + + + aes(val: string) { + if (!this.cryptoJs) { + throw new Error("crypto-js not init"); + } + const CryptoJS = this.cryptoJs; + var k = CryptoJS.enc.Utf8.parse("1234567890abcDEF"); + var iv = CryptoJS.enc.Utf8.parse("1234567890abcDEF"); + return CryptoJS.AES.encrypt(val, k, { + iv: iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.ZeroPadding + }).toString(); + } + + + async init() { + if (this.cryptoJs) { + return; + } + const CryptoJSModule = await import("crypto-js"); + this.cryptoJs = CryptoJSModule.default; + + } + + async login() { + if (this.isLogined) { + return; + } + await this.init(); + const res = await this.http.request({ + url: "https://www.51dns.com/login.html", + method: "get", + withCredentials: true, + logRes:false, + returnResponse:true + }); + + //提取 var csrfToken = "ieOfM21eDd9nWJv3OZtMJF6ogDsnPKQHJ17dlMck"; + const _token = res.data.match(/var csrfToken = "(.*?)"/)[1]; + this.logger.info("_token:", _token); + this._token = _token; + var obj = { + "email_or_phone": this.aes("18603046467"), + "password": this.aes("JiDian1Zu"), + "type": this.aes("account"), + "redirectTo": "https://www.51dns.com/domain", + "_token": _token + }; + const res2 = await this.http.request({ + url: "https://www.51dns.com/login", + method: "post", + data: { + ...obj + }, + withCredentials: true, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + logRes:false, + returnResponse:true, + }); + + + // 提取 182****43522
+ // console.log(res2.headers) + // console.log(res2.data) + const username = res2.data.match(/(.*?)<\/span>/)[1]; + + this.logger.info("登录成功:username:", username); + this.isLogined = true; + } + + async getDomainId(domain: string) { + await this.login(); + + const res = await this.http.request({ + url: `https://www.51dns.com/domain?domain=${domain}&status=`, + method: "get", + withCredentials: true, + logRes:false, + returnResponse:true + }); + + // 提取 certd.top + const regex = new RegExp(``, "g"); + const matched = res.data.match(regex); + if (!matched || matched.length === 0) { + throw new Error(`域名${domain}不存在`); + } + return matched[1]; + + } + + async createRecord(param: { domain: string, data: any; domainId: void; host: string; ttl: number; type: string }) { + const { domain, data, host, type } = param; + const domainId = await this.getDomainId(domain); + const url = "https://www.51dns.com/domain/storenNewRecord"; + const req = { + _token: this._token, + domain_id: parseInt(domainId), + record: host, + type: type, + value: data, + ttl: 300, + view_id: 0 + }; + const res = await this.http.request({ + url, + method: "post", + data: req, + withCredentials: true, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }); + + /* + { + "status": 200, + "msg": "\u6b63\u786e", + "data": { + "record": "1111", + "type": "TXT", + "value": "2222", + "mx": "-", + "ttl": "300", + "view_id": "0", + "id": 601019779, + "domain_id": "193341603", + "trecord": "1111", + "view_name": "\u9ed8\u8ba4" + } +} + */ + if(res.status !== 200){ + throw new Error(`创建域名解析失败:${res.msg}`); + } + const id = res.data.id; + return { + id, + domainId + }; + + } + + async deleteRecord(param: { domainId: number; id: number }) { + const url ="https://www.51dns.com/domain/operateRecord" + /* + type: delete +ids[0]: 601019779 +domain_id: 193341603 +_token: ieOfM21eDd9nWJv3OZtMJF6ogDsnPKQHJ17dlMck + */ + const body = { + type: "delete", + ids: [param.id], + domain_id: param.domainId, + _token: this._token + } + const res = await this.http.request({ + url, + method: "post", + data: body, + withCredentials: true, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }); + if(res.status !== 200){ + throw new Error(`删除域名解析失败:${res.msg}`); + } + + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-51dns/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-51dns/dns-provider.ts new file mode 100644 index 00000000..033524fa --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-51dns/dns-provider.ts @@ -0,0 +1,97 @@ +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; + +import { Dns51Access } from "./access.js"; +import { Dns51Client } from "./client.js"; + +export type Dns51Record = { + id: number; + domainId: number, + client: Dns51Client, +}; + +// 这里通过IsDnsProvider注册一个dnsProvider +@IsDnsProvider({ + name: '51dns', + title: '51dns', + desc: '51DNS', + icon: 'arcticons:dns-changer-3', + // 这里是对应的 cloudflare的access类型名称 + accessType: '51dns', +}) +export class Dns51DnsProvider extends AbstractDnsProvider { + // 通过Autowire传递context + access!: Dns51Access; + async onInstance() { + //一些初始化的操作 + // 也可以通过ctx成员变量传递context, 与Autowire效果一样 + this.access = this.ctx.access as Dns51Access; + } + + /** + * 创建dns解析记录,用于验证域名所有权 + */ + async createRecord(options: CreateRecordOptions): Promise { + /** + * fullRecord: '_acme-challenge.test.example.com', + * value: 一串uuid + * type: 'TXT', + * domain: 'example.com' + */ + const { fullRecord,hostRecord, value, type, domain } = options; + this.logger.info('添加域名解析:', fullRecord, value, type, domain); + + + const dns51Client = new Dns51Client({ + logger: this.logger, + access: this.access, + }); + + const domainId = await dns51Client.getDomainId(domain); + this.logger.info('获取domainId成功:', domainId); + + const res = await dns51Client.createRecord({ + domain: domain, + domainId: domainId, + type: 'TXT', + host: hostRecord, + data: value, + ttl: 300, + }) + return { + id: res.id, + domainId: domainId, + client: dns51Client, + }; + } + + + /** + * 删除dns解析记录,清理申请痕迹 + * @param options + */ + async removeRecord(options: RemoveRecordOptions): Promise { + const { fullRecord, value } = options.recordReq; + const record = options.recordRes; + this.logger.info('删除域名解析:', fullRecord, value); + if (!record) { + this.logger.info('record为空,不执行删除'); + return; + } + //这里调用删除txt dns解析记录接口 + /** + * 请求示例 + * DELETE /api/record?id=85371689655342080 HTTP/1.1 + * Authorization: Basic {token} + * 请求参数 + */ + const { client,id,domainId} = record + await client.deleteRecord({ + id, + domainId + }) + this.logger.info(`删除域名解析成功:fullRecord=${fullRecord},id=${id}`); + } +} + +//实例化这个provider,将其自动注册到系统中 +new Dns51DnsProvider(); diff --git a/packages/ui/certd-server/src/plugins/plugin-51dns/index.ts b/packages/ui/certd-server/src/plugins/plugin-51dns/index.ts new file mode 100644 index 00000000..3a9e1743 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-51dns/index.ts @@ -0,0 +1,3 @@ +export * from './dns-provider.js'; +export * from './access.js'; +export * from './client.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8bc7dae..4286cb03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1566,6 +1566,9 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 dayjs: specifier: ^1.11.7 version: 1.11.13 @@ -20673,13 +20676,13 @@ snapshots: resolve: 1.22.10 semver: 6.3.1 - eslint-plugin-prettier@3.4.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@7.32.0)(prettier@2.8.8): + eslint-plugin-prettier@3.4.1(eslint-config-prettier@8.10.0(eslint@7.32.0))(eslint@7.32.0)(prettier@2.8.8): dependencies: eslint: 7.32.0 prettier: 2.8.8 prettier-linter-helpers: 1.0.0 optionalDependencies: - eslint-config-prettier: 8.10.0(eslint@8.57.0) + eslint-config-prettier: 8.10.0(eslint@7.32.0) eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@2.8.8): dependencies: @@ -23393,7 +23396,7 @@ snapshots: eslint: 7.32.0 eslint-config-prettier: 8.10.0(eslint@7.32.0) eslint-plugin-node: 11.1.0(eslint@7.32.0) - eslint-plugin-prettier: 3.4.1(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@7.32.0)(prettier@2.8.8) + eslint-plugin-prettier: 3.4.1(eslint-config-prettier@8.10.0(eslint@7.32.0))(eslint@7.32.0)(prettier@2.8.8) execa: 5.1.1 inquirer: 7.3.3 json5: 2.2.3