diff --git a/package.json b/package.json index e4d04dd..79eecf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "filecat", - "version": "1.0.8", + "version": "1.0.9", "description": "filecat 文件管理器", "author": "xiaobaidadada", "scripts": { @@ -118,7 +118,6 @@ "style-loader": "^3.3.4", "systeminformation": "^5.22.11", "tar": "^7.4.3", - "tencentcloud-sdk-nodejs": "^4.0.858", "tldts": "^6.1.20", "ts-node": "^10.9.2", "typedi": "^0.10.0", diff --git a/src/common/frame/ws.server.ts b/src/common/frame/ws.server.ts index f6091cf..96798f5 100644 --- a/src/common/frame/ws.server.ts +++ b/src/common/frame/ws.server.ts @@ -4,7 +4,6 @@ import {CmdType, protocolIsProto2, WsConnectType, WsData} from "./WsData"; import * as parser from "socket.io-parser" import {Decoder, Encoder, Packet, PacketType} from "socket.io-parser" import {settingService} from "../../main/domain/setting/setting.service"; -import {wss} from "tencentcloud-sdk-nodejs"; const url = require('url'); diff --git a/src/common/req/sys.pojo.ts b/src/common/req/sys.pojo.ts index bf5922f..2b18cf2 100644 --- a/src/common/req/sys.pojo.ts +++ b/src/common/req/sys.pojo.ts @@ -12,6 +12,7 @@ export interface staticSysPojo { cpu_core_num:number, cpu_phy_core_num:number cpu_speed_hz:number + pid_ppid:string; } // 物理信息 diff --git a/src/main/domain/ddns/ddns.dnspod.service.ts b/src/main/domain/ddns/ddns.dnspod.service.ts index 7e95cd8..80abe95 100644 --- a/src/main/domain/ddns/ddns.dnspod.service.ts +++ b/src/main/domain/ddns/ddns.dnspod.service.ts @@ -82,11 +82,13 @@ export class DnsPodService extends DdnsPre implements updateDns{ async_have = true; DataUtil.set(ddns_dnspod_key, data); } + } else { + return false; } } finally { } - + return true; } async update(data: DdnsConnection,domain:IResult,ip:string,type:string) { diff --git a/src/main/domain/ddns/ddns.pre.ts b/src/main/domain/ddns/ddns.pre.ts index 5729eaf..513248d 100644 --- a/src/main/domain/ddns/ddns.pre.ts +++ b/src/main/domain/ddns/ddns.pre.ts @@ -115,11 +115,13 @@ export abstract class DdnsPre implements updateDns{ this.async_have = true; DataUtil.set(this.getDdnsKey(), data); } + } else { + return false; } }catch (e) { console.log(e) } - + return true; } abstract update(data: DdnsConnection, domain: IResult, ip: string, type: string); diff --git a/src/main/domain/ddns/ddns.service.ts b/src/main/domain/ddns/ddns.service.ts index 2f661a0..f06262f 100644 --- a/src/main/domain/ddns/ddns.service.ts +++ b/src/main/domain/ddns/ddns.service.ts @@ -9,29 +9,41 @@ import {ddns_dnspod_key, dnspodService} from "./ddns.dnspod.service"; import {DdnsPre} from "./ddns.pre"; import {IResult} from "tldts-core"; import {ddns_tx_key, tengxunService} from "./ddns.tengxun.service"; -import {aliService, ddns_ali_key, generateAliSignature,alidnsEndpoint} from "./ddns.ali.server"; -const tencentcloud = require("tencentcloud-sdk-nodejs") -const txClient = tencentcloud.dnspod.v20210323.Client; +import {aliService, ddns_ali_key, generateAliSignature, alidnsEndpoint} from "./ddns.ali.server"; +// const tencentcloud = require("tencentcloud-sdk-nodejs") +// const txClient = tencentcloud.dnspod.v20210323.Client; +import {Client as txClient} from "./tx/dnspod_client" const dnspodTest = "https://dnsapi.cn/User.Detail"; +export class DdnsService extends DdnsPre { -export class DdnsService extends DdnsPre{ - - public ddnsTask() { - setInterval(async ()=>{ - const netList = await this.updateAndGetIps(); - await dnspodService.Run(netList) - await tengxunService.Run(netList); - await aliService.Run(netList); + public ddnsTask() { + const ok = setInterval(() => { + (async () => { + const netList = await this.updateAndGetIps(); + let handle_num = 0; + if (await dnspodService.Run(netList)) { + handle_num++; + } + if (await tengxunService.Run(netList)) { + handle_num++; + } + if (handle_num == 0) { + clearInterval(ok); + } + })().catch(e => { + // todo 日志功能暂时不加 + console.log(e) + }) // 五分钟分钟检测一次 - },1000*60*10); + }, 1000*60*10); } - async getIps(type:string) { - const list:DdnsIPPojo[] = await this.getNowIps(); + async getIps(type: string) { + const list: DdnsIPPojo[] = await this.getNowIps(); let key = ""; switch (type) { case "dnspod": @@ -46,11 +58,11 @@ export class DdnsService extends DdnsPre{ } const result = new DdnsConnection(); const data = await DataUtil.get(key); - result.ips=list; - if (!!data && !!data.ips && data.ips.length>0) { - const map = getMapByList(data.ips,(v)=>v.ifaceOrWww+v.isIPv4); + result.ips = list; + if (!!data && !!data.ips && data.ips.length > 0) { + const map = getMapByList(data.ips, (v) => v.ifaceOrWww + v.isIPv4); for (const ip of result.ips) { - const item = map.get(ip.ifaceOrWww+ip.isIPv4); + const item = map.get(ip.ifaceOrWww + ip.isIPv4); if (item) { ip.ddnsHost = item.ddnsHost; } @@ -63,7 +75,7 @@ export class DdnsService extends DdnsPre{ return Sucess(result); } - async save(data:DdnsConnection) { + async save(data: DdnsConnection) { let key = ""; switch (data.ddnsType) { case DdnsType.dnspod: @@ -77,57 +89,59 @@ export class DdnsService extends DdnsPre{ break; } if (data.isOpen) { - if (data.ddnsType===DdnsType.dnspod) { - key = ddns_dnspod_key; - try { - const r = await HttpRequest.post(dnspodTest,{ - format:"json", - "login_token":`${(data.account as DnsPod).id},${(data.account as DnsPod).token}` - },true) - if (r.status.code+"" !== "1") { - return Sucess("身份验证失败",RCode.DdnsAuthFail); - } - } catch (e) { - return Sucess("身份验证失败",RCode.DdnsAuthFail); + if (data.ddnsType === DdnsType.dnspod) { + key = ddns_dnspod_key; + try { + const r = await HttpRequest.post(dnspodTest, { + format: "json", + "login_token": `${(data.account as DnsPod).id},${(data.account as DnsPod).token}` + }, true) + if (r.status.code + "" !== "1") { + return Sucess("身份验证失败", RCode.DdnsAuthFail); } + } catch (e) { + return Sucess("身份验证失败", RCode.DdnsAuthFail); + } - } else if (data.ddnsType===DdnsType.tengxun) { - key = ddns_tx_key; - try { - const client = new txClient({ - credential: { - secretId: (data.account as Tengxun).secretid, - secretKey: (data.account as Tengxun).secretkey - }, - }); - await client.DescribeUserDetail(); - } catch (e) { - return Sucess("身份验证失败",RCode.DdnsAuthFail); - } - } else if (data.ddnsType===DdnsType.ali) { - key = ddns_ali_key; - try { - const params = { - Action: 'DescribeTags', - AccessKeyId: (data.account as Ali).accesskey_id, - SignatureMethod: 'HMAC-SHA1', - SignatureNonce: Math.random().toString(), - Timestamp: new Date().toISOString(), - Format: 'JSON', - SignatureVersion: '1.0', - Version: '2015-01-09', - ResourceType:"DOMAIN" - }; - params['Signature'] = generateAliSignature(params, (data.account as Ali).accesskey_secret); - const req = await HttpRequest.get(alidnsEndpoint,params); - } catch (e) { - return Sucess("身份验证失败",RCode.DdnsAuthFail); - } + } else if (data.ddnsType === DdnsType.tengxun) { + key = ddns_tx_key; + try { + const client = new txClient({ + credential: { + secretId: (data.account as Tengxun).secretid, + secretKey: (data.account as Tengxun).secretkey + }, + }); + await client.DescribeUserDetail(); + } catch (e) { + return Sucess("身份验证失败", RCode.DdnsAuthFail); + } + } else if (data.ddnsType === DdnsType.ali) { + // todo 目前不用 + key = ddns_ali_key; + try { + const params = { + Action: 'DescribeTags', + AccessKeyId: (data.account as Ali).accesskey_id, + SignatureMethod: 'HMAC-SHA1', + SignatureNonce: Math.random().toString(), + Timestamp: new Date().toISOString(), + Format: 'JSON', + SignatureVersion: '1.0', + Version: '2015-01-09', + ResourceType: "DOMAIN" + }; + params['Signature'] = generateAliSignature(params, (data.account as Ali).accesskey_secret); + const req = await HttpRequest.get(alidnsEndpoint, params); + } catch (e) { + return Sucess("身份验证失败", RCode.DdnsAuthFail); } - await DataUtil.set(key,data); + } + ddnsService.ddnsTask(); + await DataUtil.set(key, data); return Sucess("身份信息正确"); } - await DataUtil.set(key,data); + await DataUtil.set(key, data); return Sucess("保存成功"); } @@ -137,5 +151,6 @@ export class DdnsService extends DdnsPre{ update(data: DdnsConnection, domain: IResult, ip: string, type: string) { } } + export const ddnsService = new DdnsService(); ddnsService.ddnsTask(); \ No newline at end of file diff --git a/src/main/domain/ddns/ddns.tengxun.service.ts b/src/main/domain/ddns/ddns.tengxun.service.ts index 920533f..ab9c97f 100644 --- a/src/main/domain/ddns/ddns.tengxun.service.ts +++ b/src/main/domain/ddns/ddns.tengxun.service.ts @@ -2,8 +2,10 @@ import {DdnsConnection, DnsPod, Tengxun} from "../../../common/req/ddns.pojo"; import {DdnsPre} from "./ddns.pre"; import {IResult} from "tldts-core"; -const tencentcloud = require("tencentcloud-sdk-nodejs") -const txClient = tencentcloud.dnspod.v20210323.Client; +// const tencentcloud = require("tencentcloud-sdk-nodejs") +// const txClient = tencentcloud.dnspod.v20210323.Client; +import {Client as txClient} from "./tx/dnspod_client" + export const ddns_tx_key = "tengxun_ddns_key" export class TengxunService extends DdnsPre { @@ -27,11 +29,12 @@ export class TengxunService extends DdnsPre { // 更新 // const record = req.RecordList[0]; await client.ModifyRecord({ + RecordId:req.RecordList[0].RecordId, "RecordType": type, "SubDomain": domain.subdomain, "Domain": domain.domain, "Value": ip, - "RecordLine": "默认", + "RecordLine": "默认" }); } else { //添加 diff --git a/src/main/domain/ddns/tx/abstract_client.ts b/src/main/domain/ddns/tx/abstract_client.ts new file mode 100644 index 0000000..7cfaecb --- /dev/null +++ b/src/main/domain/ddns/tx/abstract_client.ts @@ -0,0 +1,376 @@ +import { sdkVersion } from "./sdk_version" +import { + ClientProfile, + Credential, + ClientConfig, + SUPPORT_LANGUAGE_LIST, + HttpProfile, + DynamicCredential, +} from "./interface" +import Sign from "./sign" +import { HttpConnection } from "./http/http_connection" +import TencentCloudSDKHttpException from "./exception/tencent_cloud_sdk_exception" +import { SSEResponseModel } from "./sse_response_model" +import { v4 as uuidv4 } from "uuid" + +export type ResponseCallback = (error: string, rep: TReuslt) => void +export interface RequestOptions extends Partial> { + multipart?: boolean + /** + * 中止请求信号 + */ + signal?: AbortSignal +} + +interface RequestData { + Action: string + RequestClient: string + Nonce: number + Timestamp: number + Version: string + Signature: string + SecretId?: string + region?: string + Token?: string + SinatureMethod?: string + [key: string]: any +} +type ResponseData = any + +/** + * @inner + */ +export class AbstractClient { + sdkVersion: string + path: string + credential: Credential | DynamicCredential + region: string + apiVersion: string + endpoint: string + profile: ClientProfile + /** + * 实例化client对象 + * @param {string} endpoint 接入点域名 + * @param {string} version 产品版本 + * @param {Credential} credential 认证信息实例 + * @param {string} region 产品地域 + * @param {ClientProfile} profile 可选配置实例 + */ + constructor( + endpoint: string, + version: string, + { credential, region, profile = {} }: ClientConfig + ) { + this.path = "/" + + /** + * 认证信息实例 + */ + if (credential && "getCredential" in credential) { + this.credential = credential + } else { + this.credential = Object.assign( + { + secretId: null, + secretKey: null, + token: null, + }, + credential + ) + } + + /** + * 产品地域 + */ + this.region = region || null + this.sdkVersion = "SDK_NODEJS_" + sdkVersion + this.apiVersion = version + this.endpoint = (profile && profile.httpProfile && profile.httpProfile.endpoint) || endpoint + + /** + * 可选配置实例 + * @type {ClientProfile} + */ + this.profile = { + signMethod: (profile && profile.signMethod) || "TC3-HMAC-SHA256", + httpProfile: Object.assign( + { + reqMethod: "POST", + endpoint: null, + protocol: "https://", + reqTimeout: 60, + }, + profile && profile.httpProfile + ), + language: profile.language, + } + + if (this.profile.language && !SUPPORT_LANGUAGE_LIST.includes(this.profile.language)) { + throw new TencentCloudSDKHttpException( + `Language invalid, choices: ${SUPPORT_LANGUAGE_LIST.join("|")}` + ) + } + } + + async getCredential(): Promise { + if ("getCredential" in this.credential) { + return await this.credential.getCredential() + } + return this.credential + } + + /** + * @inner + */ + async request( + action: string, + req: any, + options?: ResponseCallback | RequestOptions, + cb?: ResponseCallback + ): Promise { + if (typeof options === "function") { + cb = options + options = {} as RequestOptions + } + try { + const result = await this.doRequest(action, req ?? {}, options as RequestOptions) + cb && cb(null, result) + return result + } catch (e) { + cb && cb(e as any, null) + throw e + } + } + + /** + * @inner + */ + async requestOctetStream( + action: string, + req: any, + options?: ResponseCallback | RequestOptions, + cb?: ResponseCallback + ) { + if (typeof options === "function") { + cb = options + options = {} as RequestOptions + } + + try { + const result = await this.doRequest( + action, + req ?? {}, + Object.assign({}, options, { + headers: { + "Content-Type": "application/octet-stream; charset=utf-8", + }, + }) + ) + cb && cb(null, result) + return result + } catch (e) { + cb && cb(e as any, null) + throw e + } + } + + /** + * @inner + */ + private async doRequest( + action: string, + req: any, + options: RequestOptions = {} + ): Promise { + if (this.profile.signMethod === "TC3-HMAC-SHA256") { + return this.doRequestWithSign3(action, req, options) + } + let params = this.mergeData(req) + params = await this.formatRequestData(action, params) + + const headers = Object.assign({}, this.profile.httpProfile.headers, options.headers) + let traceId = "" + for (let key in headers) { + if (key.toLowerCase() === "x-tc-traceid") { + traceId = headers[key] + break + } + } + if (!traceId) { + traceId = uuidv4() + headers["X-TC-TraceId"] = traceId + } + + let res + try { + res = await HttpConnection.doRequest({ + method: this.profile.httpProfile.reqMethod, + url: this.profile.httpProfile.protocol + this.endpoint + this.path, + data: params, + timeout: this.profile.httpProfile.reqTimeout * 1000, + headers, + agent: this.profile.httpProfile.agent, + proxy: this.profile.httpProfile.proxy, + signal: options.signal, + }) + } catch (error) { + throw new TencentCloudSDKHttpException((error as any).message, "", traceId) + } + return this.parseResponse(res) + } + + /** + * @inner + */ + private async doRequestWithSign3( + action: string, + params: any, + options: RequestOptions = {} + ): Promise { + const headers = Object.assign({}, this.profile.httpProfile.headers, options.headers) + let traceId = "" + for (let key in headers) { + if (key.toLowerCase() === "x-tc-traceid") { + traceId = headers[key] + break + } + } + if (!traceId) { + traceId = uuidv4() + headers["X-TC-TraceId"] = traceId + } + + let res + try { + const credential = await this.getCredential() + res = await HttpConnection.doRequestWithSign3({ + method: this.profile.httpProfile.reqMethod, + url: this.profile.httpProfile.protocol + this.endpoint + this.path, + secretId: credential.secretId, + secretKey: credential.secretKey, + region: this.region, + data: params || "", + service: this.endpoint.split(".")[0], + action: action, + version: this.apiVersion, + multipart: options && options.multipart, + timeout: this.profile.httpProfile.reqTimeout * 1000, + token: credential.token, + requestClient: this.sdkVersion, + language: this.profile.language, + headers, + agent: this.profile.httpProfile.agent, + proxy: this.profile.httpProfile.proxy, + signal: options.signal, + }) + } catch (e) { + throw new TencentCloudSDKHttpException((e as any).message, "", traceId) + } + return this.parseResponse(res) + } + + private async parseResponse(res: Response): Promise { + const traceId = res.headers.get("x-tc-traceid") + if (res.status !== 200) { + const tcError = new TencentCloudSDKHttpException(res.statusText, "", traceId) + tcError.httpCode = res.status + throw tcError + } else { + if (res.headers.get("content-type") === "text/event-stream") { + // @ts-ignore + return new SSEResponseModel(res.body) + } else { + const data:any = await res.json() + if (data.Response.Error) { + const tcError = new TencentCloudSDKHttpException( + data.Response.Error.Message, + data.Response.RequestId, + traceId + ) + tcError.code = data.Response.Error.Code + throw tcError + } else { + return data.Response + } + } + } + } + + /** + * @inner + */ + private mergeData(data: any, prefix = "") { + const ret: any = {} + for (const k in data) { + if (data[k] === null || data[k] === undefined) { + continue + } + if (data[k] instanceof Array || data[k] instanceof Object) { + Object.assign(ret, this.mergeData(data[k], prefix + k + ".")) + } else { + ret[prefix + k] = data[k] + } + } + return ret + } + + /** + * @inner + */ + private async formatRequestData(action: string, params: RequestData): Promise { + params.Action = action + params.RequestClient = this.sdkVersion + params.Nonce = Math.round(Math.random() * 65535) + params.Timestamp = Math.round(Date.now() / 1000) + params.Version = this.apiVersion + + const credential = await this.getCredential() + + if (credential.secretId) { + params.SecretId = credential.secretId + } + + if (this.region) { + params.Region = this.region + } + + if (credential.token) { + params.Token = credential.token + } + + if (this.profile.language) { + params.Language = this.profile.language + } + + if (this.profile.signMethod) { + params.SignatureMethod = this.profile.signMethod + } + const signStr = this.formatSignString(params) + + params.Signature = Sign.sign(credential.secretKey, signStr, this.profile.signMethod) + return params + } + + /** + * @inner + */ + private formatSignString(params: RequestData): string { + let strParam = "" + const keys = Object.keys(params) + keys.sort() + for (const k in keys) { + if (!keys.hasOwnProperty(k)) { + continue + } + //k = k.replace(/_/g, '.'); + strParam += "&" + keys[k] + "=" + params[keys[k]] + } + const strSign = + this.profile.httpProfile.reqMethod.toLocaleUpperCase() + + this.endpoint + + this.path + + "?" + + strParam.slice(1) + return strSign + } +} diff --git a/src/main/domain/ddns/tx/dnspod_client.ts b/src/main/domain/ddns/tx/dnspod_client.ts new file mode 100644 index 0000000..e3dd6f0 --- /dev/null +++ b/src/main/domain/ddns/tx/dnspod_client.ts @@ -0,0 +1,146 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* + * Copyright (c) 2018 THL A29 Limited, a Tencent company. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {ClientConfig} from "./interface" +import { + CreateRecordRequest, + DescribeDomainResponse, + ModifyRecordResponse, + DeleteDomainRequest, + DescribeRecordListResponse, + DescribeRecordRequest, + DeleteRecordRequest, + DescribeDomainRequest, + CreateRecordResponse, + DescribeRecordResponse, + DeleteRecordResponse, + DescribeUserDetailResponse, + DescribeRecordListRequest, + DeleteDomainResponse, + ModifyDynamicDNSRequest, + DescribeUserDetailRequest, + ModifyDynamicDNSResponse, + ModifyRecordRequest, +} from "./dnspod_models" +import {AbstractClient} from "./abstract_client"; + +/** + * dnspod client + * @class + */ +export class Client extends AbstractClient { + constructor(clientConfig: ClientConfig) { + super("dnspod.tencentcloudapi.com", "2021-03-23", clientConfig) + } + + + /** + * 获取某个域名下的解析记录列表 + 备注: + 1. 新添加的解析记录存在短暂的索引延迟,如果查询不到新增记录,请在 30 秒后重试 + 2. API获取的记录总条数会比控制台多2条,原因是: 为了防止用户误操作导致解析服务不可用,对2021-10-29 14:24:26之后添加的域名,在控制台都不显示这2条NS记录。 + */ + async DescribeRecordList( + req: DescribeRecordListRequest, + cb?: (error: string, rep: DescribeRecordListResponse) => void + ): Promise { + return this.request("DescribeRecordList", req, cb) + } + + /** + * 修改记录 + */ + async ModifyRecord( + req: ModifyRecordRequest, + cb?: (error: string, rep: ModifyRecordResponse) => void + ): Promise { + return this.request("ModifyRecord", req, cb) + } + + /** + * 添加记录 + 备注:新添加的解析记录存在短暂的索引延迟,如果查询不到新增记录,请在 30 秒后重试 + */ + async CreateRecord( + req: CreateRecordRequest, + cb?: (error: string, rep: CreateRecordResponse) => void + ): Promise { + return this.request("CreateRecord", req, cb) + } + + /** + * 获取账户信息 + */ + async DescribeUserDetail( + req?: DescribeUserDetailRequest, + cb?: (error: string, rep: DescribeUserDetailResponse) => void + ): Promise { + return this.request("DescribeUserDetail", req, cb) + } + + + /** + * 获取记录信息 + */ + async DescribeRecord( + req: DescribeRecordRequest, + cb?: (error: string, rep: DescribeRecordResponse) => void + ): Promise { + return this.request("DescribeRecord", req, cb) + } + + /** + * 更新动态 DNS 记录 + */ + async ModifyDynamicDNS( + req: ModifyDynamicDNSRequest, + cb?: (error: string, rep: ModifyDynamicDNSResponse) => void + ): Promise { + return this.request("ModifyDynamicDNS", req, cb) + } + + /** + * 获取域名信息 + */ + async DescribeDomain( + req: DescribeDomainRequest, + cb?: (error: string, rep: DescribeDomainResponse) => void + ): Promise { + return this.request("DescribeDomain", req, cb) + } + + /** + * 删除记录 + */ + async DeleteRecord( + req: DeleteRecordRequest, + cb?: (error: string, rep: DeleteRecordResponse) => void + ): Promise { + return this.request("DeleteRecord", req, cb) + } + + /** + * 删除域名 + */ + async DeleteDomain( + req: DeleteDomainRequest, + cb?: (error: string, rep: DeleteDomainResponse) => void + ): Promise { + return this.request("DeleteDomain", req, cb) + } +} diff --git a/src/main/domain/ddns/tx/dnspod_models.ts b/src/main/domain/ddns/tx/dnspod_models.ts new file mode 100644 index 0000000..e67275c --- /dev/null +++ b/src/main/domain/ddns/tx/dnspod_models.ts @@ -0,0 +1,812 @@ +/* + * Copyright (c) 2018 THL A29 Limited, a Tencent company. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * CreateRecord请求参数结构体 + */ +export interface CreateRecordRequest { + /** + * 域名 + */ + Domain: string + /** + * 记录类型,通过 API 记录类型获得,大写英文,比如:A 。 + */ + RecordType: string + /** + * 记录线路,通过 API 记录线路获得,中文,比如:默认。 + */ + RecordLine: string + /** + * 记录值,如 IP : 200.200.200.200, CNAME : cname.dnspod.com., MX : mail.dnspod.com.。 + */ + Value: string + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。 + */ + DomainId?: number + /** + * 主机记录,如 www,如果不传,默认为 @。 + */ + SubDomain?: string + /** + * 线路的 ID,通过 API 记录线路获得,英文字符串,比如:10=1。参数RecordLineId优先级高于RecordLine,如果同时传递二者,优先使用RecordLineId参数。 + */ + RecordLineId?: string + /** + * MX 优先级,当记录类型是 MX 时有效,范围1-20,MX 记录时必选。 + */ + MX?: number + /** + * TTL,范围1-604800,不同套餐域名最小值不同。 + */ + TTL?: number + /** + * 权重信息,0到100的整数。0 表示关闭,不传该参数,表示不设置权重信息。 + */ + Weight?: number + /** + * 记录初始状态,取值范围为 ENABLE 和 DISABLE 。默认为 ENABLE ,如果传入 DISABLE,解析不会生效,也不会验证负载均衡的限制。 + */ + Status?: string + /** + * 备注 + */ + Remark?: string + /** + * 开启DNSSEC时,强制添加CNAME/URL记录 + */ + DnssecConflictMode?: string +} + + +/** + * DescribeDomain返回参数结构体 + */ +export interface DescribeDomainResponse { + /** + * 域名信息 + */ + DomainInfo?: DomainInfo + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + +/** + * 域名详情 + */ +export interface DomainInfo { + /** + * 域名ID + */ + DomainId?: number + /** + * 域名状态 + */ + Status?: string + /** + * 域名套餐等级 + */ + Grade?: string + /** + * 域名分组ID + */ + GroupId?: number + /** + * 是否星标域名 + */ + IsMark?: string + /** + * TTL(DNS记录缓存时间) + */ + TTL?: number + /** + * cname加速启用状态 + */ + CnameSpeedup?: string + /** + * 域名备注 +注意:此字段可能返回 null,表示取不到有效值。 + */ + Remark?: string + /** + * 域名Punycode + */ + Punycode?: string + /** + * 域名DNS状态 + */ + DnsStatus?: string + /** + * 域名的NS列表 + */ + DnspodNsList?: Array + /** + * 域名 + */ + Domain?: string + /** + * 域名等级代号 + */ + GradeLevel?: number + /** + * 域名所属的用户ID + */ + UserId?: number + /** + * 是否为付费域名 + */ + IsVip?: string + /** + * 域名所有者的账号 + */ + Owner?: string + /** + * 域名等级的描述 + */ + GradeTitle?: string + /** + * 域名创建时间 + */ + CreatedOn?: string + /** + * 最后操作时间 + */ + UpdatedOn?: string + /** + * 腾讯云账户Uin + */ + Uin?: string + /** + * 域名实际使用的NS列表 +注意:此字段可能返回 null,表示取不到有效值。 + */ + ActualNsList?: Array + /** + * 域名的记录数量 + */ + RecordCount?: number + /** + * 域名所有者的账户昵称 +注意:此字段可能返回 null,表示取不到有效值。 + */ + OwnerNick?: string + /** + * 是否在付费套餐宽限期 +注意:此字段可能返回 null,表示取不到有效值。 + */ + IsGracePeriod?: string + /** + * 是否在付费套餐缓冲期 +注意:此字段可能返回 null,表示取不到有效值。 + */ + VipBuffered?: string + /** + * VIP套餐有效期开始时间 +注意:此字段可能返回 null,表示取不到有效值。 + */ + VipStartAt?: string + /** + * VIP套餐有效期结束时间 +注意:此字段可能返回 null,表示取不到有效值。 + */ + VipEndAt?: string + /** + * VIP套餐自动续费标识。可能的值为:default-默认;no-不自动续费;yes-自动续费 +注意:此字段可能返回 null,表示取不到有效值。 + */ + VipAutoRenew?: string + /** + * VIP套餐资源ID +注意:此字段可能返回 null,表示取不到有效值。 + */ + VipResourceId?: string + /** + * 是否是子域名。 +注意:此字段可能返回 null,表示取不到有效值。 + */ + IsSubDomain?: boolean + /** + * 域名关联的标签列表 +注意:此字段可能返回 null,表示取不到有效值。 + */ + TagList?: Array + /** + * 是否启用搜索引擎推送 + */ + SearchEnginePush?: string + /** + * 是否开启辅助 DNS + */ + SlaveDNS?: string +} + + + + +/** + * ModifyRecord返回参数结构体 + */ +export interface ModifyRecordResponse { + /** + * 记录ID + */ + RecordId?: number + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * 查询记录列表的数量统计信息 + */ +export interface RecordCountInfo { + /** + * 子域名数量 + */ + SubdomainCount: number + /** + * 列表返回的记录数 + */ + ListCount: number + /** + * 总的记录数 + */ + TotalCount: number +} + + +/** + * DeleteDomain请求参数结构体 + */ +export interface DeleteDomainRequest { + /** + * 域名 + */ + Domain: string + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number +} + +/** + * DescribeRecordList返回参数结构体 + */ +export interface DescribeRecordListResponse { + /** + * 记录的数量统计信息 + */ + RecordCountInfo?: RecordCountInfo + /** + * 获取的记录列表 + */ + RecordList?: Array + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * DescribeRecord请求参数结构体 + */ +export interface DescribeRecordRequest { + /** + * 域名 + */ + Domain: string + /** + * 记录 ID 。可以通过接口DescribeRecordList查到所有的解析记录列表以及对应的RecordId + */ + RecordId: number + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number +} + +/** + * DeleteRecord请求参数结构体 + */ +export interface DeleteRecordRequest { + /** + * 域名 + */ + Domain: string + /** + * 记录 ID 。可以通过接口DescribeRecordList查到所有的解析记录列表以及对应的RecordId + */ + RecordId: number + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number +} + +/** + * DescribeDomain请求参数结构体 + */ +export interface DescribeDomainRequest { + /** + * 域名 + */ + Domain: string + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number +} + + + +/** + * CreateRecord返回参数结构体 + */ +export interface CreateRecordResponse { + /** + * 记录ID + */ + RecordId?: number + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * 记录信息 + */ +export interface RecordInfo { + /** + * 记录 ID 。 + */ + Id: number + /** + * 子域名(主机记录)。 + */ + SubDomain: string + /** + * 记录类型, 详见 DescribeRecordType 接口。 + */ + RecordType: string + /** + * 解析记录的线路,详见 DescribeRecordLineList 接口。 + */ + RecordLine: string + /** + * 解析记录的线路 ID ,详见 DescribeRecordLineList 接口。 + */ + RecordLineId: string + /** + * 记录值。 + */ + Value: string + /** + * 记录权重值。 +注意:此字段可能返回 null,表示取不到有效值。 + */ + Weight: number + /** + * 记录的 MX 记录值,非 MX 记录类型,默认为 0。 + */ + MX: number + /** + * 记录的 TTL 值。 + */ + TTL: number + /** + * 记录状态。0表示禁用,1表示启用。 + */ + Enabled: number + /** + * 该记录的 D 监控状态。 +"Ok" : 服务器正常。 +"Warn" : 该记录有报警, 服务器返回 4XX。 +"Down" : 服务器宕机。 +"" : 该记录未开启 D 监控。 + */ + MonitorStatus: string + /** + * 记录的备注。 +注意:此字段可能返回 null,表示取不到有效值。 + */ + Remark: string + /** + * 记录最后更新时间。 + */ + UpdatedOn: string + /** + * 域名 ID 。 + */ + DomainId: number +} + +/** + * 用户信息 + */ +export interface UserInfo { + /** + * 用户昵称 + */ + Nick: string + /** + * 用户ID + */ + Id: number + /** + * 用户账号, 邮箱格式 + */ + Email: string + /** + * 账号状态:”enabled”: 正常;”disabled”: 被封禁 + */ + Status: string + /** + * 电话号码 + */ + Telephone: string + /** + * 邮箱是否通过验证:”yes”: 通过;”no”: 未通过 + */ + EmailVerified: string + /** + * 手机是否通过验证:”yes”: 通过;”no”: 未通过 + */ + TelephoneVerified: string + /** + * 账号等级, 按照用户账号下域名等级排序, 选取一个最高等级为账号等级, 具体对应情况参见域名等级。 + */ + UserGrade: string + /** + * 用户名称, 企业用户对应为公司名称 + */ + RealName: string + /** + * 是否绑定微信:”yes”: 通过;”no”: 未通过 + */ + WechatBinded: string + /** + * 用户UIN + */ + Uin: number + /** + * 所属 DNS 服务器 + */ + FreeNs: Array +} + + +/** + * DescribeRecord返回参数结构体 + */ +export interface DescribeRecordResponse { + /** + * 记录信息 + */ + RecordInfo?: RecordInfo + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * DeleteRecord返回参数结构体 + */ +export interface DeleteRecordResponse { + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * 记录列表元素 + */ +export interface RecordListItem { + /** + * 记录Id + */ + RecordId: number + /** + * 记录值 + */ + Value: string + /** + * 记录状态,启用:ENABLE,暂停:DISABLE + */ + Status: string + /** + * 更新时间 + */ + UpdatedOn: string + /** + * 主机名 + */ + Name: string + /** + * 记录线路 + */ + Line: string + /** + * 线路Id + */ + LineId: string + /** + * 记录类型 + */ + Type: string + /** + * 记录权重,用于负载均衡记录 +注意:此字段可能返回 null,表示取不到有效值。 + */ + Weight: number + /** + * 记录监控状态,正常:OK,告警:WARN,宕机:DOWN,未设置监控或监控暂停则为空 + */ + MonitorStatus: string + /** + * 记录备注说明 + */ + Remark: string + /** + * 记录缓存时间 + */ + TTL: number + /** + * MX值,只有MX记录有 +注意:此字段可能返回 null,表示取不到有效值。 + */ + MX: number + /** + * 是否是默认的ns记录 + */ + DefaultNS?: boolean +} + + + +/** + * DescribeUserDetail返回参数结构体 + */ +export interface DescribeUserDetailResponse { + /** + * 账户信息 + */ + UserInfo?: UserInfo + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * DescribeRecordList请求参数结构体 + */ +export interface DescribeRecordListRequest { + /** + * 要获取的解析记录所属的域名 + */ + Domain: string + /** + * 要获取的解析记录所属的域名Id,如果传了DomainId,系统将会忽略Domain参数。 可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number + /** + * 解析记录的主机头,如果传了此参数,则只会返回此主机头对应的解析记录 + */ + Subdomain?: string + /** + * 获取某种类型的解析记录,如 A,CNAME,NS,AAAA,显性URL,隐性URL,CAA,SPF等 + */ + RecordType?: string + /** + * 获取某条线路名称的解析记录。可以通过接口DescribeRecordLineList查看当前域名允许的线路信息 + */ + RecordLine?: string + /** + * 获取某个线路Id对应的解析记录,如果传RecordLineId,系统会忽略RecordLine参数。可以通过接口DescribeRecordLineList查看当前域名允许的线路信息 + */ + RecordLineId?: string + /** + * 获取某个分组下的解析记录时,传这个分组Id。 + */ + GroupId?: number + /** + * 通过关键字搜索解析记录,当前支持搜索主机头和记录值 + */ + Keyword?: string + /** + * 排序字段,支持 name,line,type,value,weight,mx,ttl,updated_on 几个字段。 + */ + SortField?: string + /** + * 排序方式,正序:ASC,逆序:DESC。默认值为ASC。 + */ + SortType?: string + /** + * 偏移量,默认值为0。 + */ + Offset?: number + /** + * 限制数量,当前Limit最大支持3000。默认值为100。 + */ + Limit?: number +} + + + + +/** + * DeleteDomain返回参数结构体 + */ +export interface DeleteDomainResponse { + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + + +/** + * ModifyDynamicDNS请求参数结构体 + */ +export interface ModifyDynamicDNSRequest { + /** + * 域名 + */ + Domain: string + /** + * 记录ID。 可以通过接口DescribeRecordList查到所有的解析记录列表以及对应的RecordId + */ + RecordId: number + /** + * 记录线路,通过 API 记录线路获得,中文,比如:默认。 + */ + RecordLine: string + /** + * 记录值,如 IP : 200.200.200.200, CNAME : cname.dnspod.com., MX : mail.dnspod.com.。 + */ + Value: string + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number + /** + * 主机记录,如 www,如果不传,默认为 @。 + */ + SubDomain?: string + /** + * 线路的 ID,通过 API 记录线路获得,英文字符串,比如:10=1。参数RecordLineId优先级高于RecordLine,如果同时传递二者,优先使用RecordLineId参数。 + */ + RecordLineId?: string + /** + * TTL值,如果不传,默认为域名的TTL值。 + */ + Ttl?: number +} + + +/** + * DescribeUserDetail请求参数结构体 + */ +export type DescribeUserDetailRequest = null + + +/** + * 标签项 + */ +export interface TagItem { + /** + * 标签键 + */ + TagKey: string + /** + * 标签值 +注意:此字段可能返回 null,表示取不到有效值。 + */ + TagValue?: string +} + + + +/** + * ModifyDynamicDNS返回参数结构体 + */ +export interface ModifyDynamicDNSResponse { + /** + * 记录ID + */ + RecordId?: number + /** + * 唯一请求 ID,由服务端生成,每次请求都会返回(若请求因其他原因未能抵达服务端,则该次请求不会获得 RequestId)。定位问题时需要提供该次请求的 RequestId。 + */ + RequestId?: string +} + +/** + * ModifyRecord请求参数结构体 + */ +export interface ModifyRecordRequest { + /** + * 域名 + */ + Domain: string + /** + * 记录类型,通过 API 记录类型获得,大写英文,比如:A 。 + */ + RecordType: string + /** + * 记录线路,通过 API 记录线路获得,中文,比如:默认。 + */ + RecordLine: string + /** + * 记录值,如 IP : 200.200.200.200, CNAME : cname.dnspod.com., MX : mail.dnspod.com.。 + */ + Value: string + /** + * 记录 ID 。可以通过接口DescribeRecordList查到所有的解析记录列表以及对应的RecordId + */ + RecordId: number + /** + * 域名 ID 。参数 DomainId 优先级比参数 Domain 高,如果传递参数 DomainId 将忽略参数 Domain 。可以通过接口DescribeDomainList查到所有的Domain以及DomainId + */ + DomainId?: number + /** + * 主机记录,如 www,如果不传,默认为 @。 + */ + SubDomain?: string + /** + * 线路的 ID,通过 API 记录线路获得,英文字符串,比如:10=1。参数RecordLineId优先级高于RecordLine,如果同时传递二者,优先使用RecordLineId参数。 + */ + RecordLineId?: string + /** + * MX 优先级,当记录类型是 MX 时有效,范围1-20,MX 记录时必选。 + */ + MX?: number + /** + * TTL,范围1-604800,不同等级域名最小值不同。 + */ + TTL?: number + /** + * 权重信息,0到100的整数。0 表示关闭,不传该参数,表示不设置权重信息。 + */ + Weight?: number + /** + * 记录初始状态,取值范围为 ENABLE 和 DISABLE 。默认为 ENABLE ,如果传入 DISABLE,解析不会生效,也不会验证负载均衡的限制。 + */ + Status?: string + /** + * 记录的备注信息。传空删除备注。 + */ + Remark?: string + /** + * 开启DNSSEC时,强制将其它记录修改为CNAME/URL记录 + */ + DnssecConflictMode?: string +} + diff --git a/src/main/domain/ddns/tx/exception/tencent_cloud_sdk_exception.ts b/src/main/domain/ddns/tx/exception/tencent_cloud_sdk_exception.ts new file mode 100644 index 0000000..ce5440a --- /dev/null +++ b/src/main/domain/ddns/tx/exception/tencent_cloud_sdk_exception.ts @@ -0,0 +1,66 @@ +/** + * @inner + */ +export default class TencentCloudSDKHttpException extends Error { + /** + * 请求id + */ + requestId: string + + /** + * 请求traceId + */ + traceId: string + + /** + * http状态码 + */ + httpCode?: number + + /** + * 接口返回状态码 + */ + code?: string + + constructor(error: string, requestId = "", traceId = "") { + super(error) + this.requestId = requestId || "" + this.traceId = traceId || "" + } + + getMessage(): string { + return this.message + } + + getRequestId(): string { + return this.requestId + } + + getTraceId(): string { + return this.traceId + } + + toString(): string { + return ( + "[TencentCloudSDKException]" + + "message:" + + this.getMessage() + + " requestId:" + + this.getRequestId() + + " traceId:" + + this.getTraceId() + ) + } + + toLocaleString(): string { + return ( + "[TencentCloudSDKException]" + + "message:" + + this.getMessage() + + " requestId:" + + this.getRequestId() + + " traceId:" + + this.getTraceId() + ) + } +} diff --git a/src/main/domain/ddns/tx/http/fetch.ts b/src/main/domain/ddns/tx/http/fetch.ts new file mode 100644 index 0000000..802c559 --- /dev/null +++ b/src/main/domain/ddns/tx/http/fetch.ts @@ -0,0 +1,9 @@ + + +export interface FetchOptions extends Omit { + proxy?: string + headers?: Record + // node-fetch中的signal声明与ts自带的有点冲突,以ts的为准 + signal?: AbortSignal +} + diff --git a/src/main/domain/ddns/tx/http/http_connection.ts b/src/main/domain/ddns/tx/http/http_connection.ts new file mode 100644 index 0000000..f639819 --- /dev/null +++ b/src/main/domain/ddns/tx/http/http_connection.ts @@ -0,0 +1,238 @@ +import * as QueryString from "querystring" +import { URL } from "url" +// import * as isStream from "is-stream" +// import * as getStream from "get-stream" +import * as FormData from "form-data" +import Sign from "../sign" +import { FetchOptions } from "./fetch" +// import { Response, RequestInit } from "node-fetch" +import { Agent } from "http" +// import * as JSONBigInt from "json-bigint" + +// const JSONbigNative = JSONBigInt({ useNativeBigInt: true }) +/** + * @inner + */ +export class HttpConnection { + static async doRequest({ + method, + url, + data, + timeout, + headers = {}, + agent, + proxy, + signal, + }: { + method: string + url: string + data: any + timeout: number + headers?: Record + agent?: Agent + proxy?: string + signal?: AbortSignal + }): Promise { + const config: FetchOptions = { + method: method, + headers: Object.assign({}, headers), + // @ts-ignore + timeout, + agent, + proxy, + signal, + } + if (method === "GET") { + url += "?" + QueryString.stringify(data) + } else { + config.headers["Content-Type"] = "application/x-www-form-urlencoded" + config.body = QueryString.stringify(data) + } + return await fetch(url, config) + } + + static async doRequestWithSign3({ + method, + url, + data, + service, + action, + region, + version, + secretId, + secretKey, + multipart = false, + timeout = 60000, + token, + requestClient, + language, + headers = {}, + agent, + proxy, + signal, + }: { + method: string + url: string + data: any + service: string + action: string + region: string + version: string + secretId: string + secretKey: string + multipart?: boolean + timeout?: number + token: string + requestClient: string + language: string + headers?: Record + agent?: Agent + proxy?: string + signal?: AbortSignal + }): Promise { + // data 中可能带有 readStream,由于需要计算整个 body 的 hash, + // 所以这里把 readStream 转为 Buffer + // eslint-disable-next-line @typescript-eslint/no-use-before-define + // await convertReadStreamToBuffer(data) + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + data = deepRemoveNull(data) + + const timestamp = parseInt(String(new Date().getTime() / 1000)) + method = method.toUpperCase() + + let payload = "" + if (method === "GET") { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + data = mergeData(data) + url += "?" + QueryString.stringify(data) + } + if (method === "POST") { + payload = data + } + const config: FetchOptions = { + method, + // @ts-ignore + timeout, + headers: Object.assign({}, headers, { + Host: new URL(url).host, + "X-TC-Action": action, + "X-TC-Region": region, + "X-TC-Timestamp": timestamp, + "X-TC-Version": version, + "X-TC-Token": token, + "X-TC-RequestClient": requestClient, + }), + agent, + proxy, + signal, + } + + if (token === null || token === undefined) { + delete config.headers["X-TC-Token"] + } + if (region === null || region === undefined) { + delete config.headers["X-TC-Region"] + } + + if (language) { + config.headers["X-TC-Language"] = language + } + + let form + if (method === "GET") { + config.headers["Content-Type"] = "application/x-www-form-urlencoded" + } + if (method === "POST" && !multipart) { + config.body = data + const contentType = config.headers["Content-Type"] || "application/json" + if (!isBuffer(data)) config.body = JSON.stringify(data) + config.headers["Content-Type"] = contentType + } + if (method === "POST" && multipart) { + // @ts-ignore + form = new FormData() + for (const key in data) { + form.append(key, data[key]) + } + config.body = form + config.headers = Object.assign({}, config.headers, form.getHeaders()) + } + + const signature = Sign.sign3({ + method, + url, + payload, + timestamp, + service, + secretId, + secretKey, + multipart, + boundary: form ? form.getBoundary() : undefined, + headers: config.headers, + }) + + config.headers["Authorization"] = signature + + return await fetch(url, config) + } +} + +// async function convertReadStreamToBuffer(data: any): Promise { +// for (const key in data) { +// // @ts-ignore +// if (isStream(data[key])) { // 需要is-stream库? +// data[key] = await getStream.buffer(data[key]) +// } +// } +// } + +function mergeData(data: any, prefix = "") { + const ret: any = {} + for (const k in data) { + if (data[k] === null) { + continue + } + if (data[k] instanceof Array || data[k] instanceof Object) { + Object.assign(ret, mergeData(data[k], prefix + k + ".")) + } else { + ret[prefix + k] = data[k] + } + } + return ret +} + +function deepRemoveNull(obj: any) { + if (isArray(obj)) { + return obj.map(deepRemoveNull) + } else if (isObject(obj)) { + const result: any = {} + for (const key in obj) { + const value = obj[key] + if (!isNull(value)) { + result[key] = deepRemoveNull(value) + } + } + return result + } else { + return obj + } +} + +function isBuffer(x: any): boolean { + return Buffer.isBuffer(x) +} + +function isArray(x: any): boolean { + return Array.isArray(x) +} + +function isObject(x: any): boolean { + // @ts-ignore + // return typeof x === "object" && !isArray(x) && !isStream(x) && !isBuffer(x) && x !== null + return typeof x === "object" && !isArray(x) && !isBuffer(x) && x !== null +} + +function isNull(x: any): boolean { + return x === null +} diff --git a/src/main/domain/ddns/tx/interface.ts b/src/main/domain/ddns/tx/interface.ts new file mode 100644 index 0000000..8cdde38 --- /dev/null +++ b/src/main/domain/ddns/tx/interface.ts @@ -0,0 +1,121 @@ +import { Agent } from "http" +/** + * 初始化client对象参数类型 + */ +export interface ClientConfig { + /** + * @param {Credential} credential 认证信息 + * 必选 + */ + credential: Credential | DynamicCredential + /** + * @param {string} region 产品地域 + * 对于要求区分地域的产品,此参数必选(如 cvm);对于不区分地域的产品(如 sms),无需传入。 + */ + region?: string + /** + * @param {ClientProfile} profile 可选配置实例 + * 可选,没有特殊需求可以跳过。 + */ + profile?: ClientProfile +} + +/** + * 可选配置实例 + */ +export interface ClientProfile { + /** + * 签名方法 (TC3-HMAC-SHA256 HmacSHA1 HmacSHA256) + * @type {string} + * 非必选 + */ + signMethod?: "TC3-HMAC-SHA256" | "HmacSHA256" | "HmacSHA1" + /** + * http相关选项实例 + * @type {HttpProfile} + * 非必选 + */ + httpProfile?: HttpProfile + /** + * api请求时附带的 language 字段 + * @type {"zh-CN" | "en-US"} + * 非必选 + */ + language?: "zh-CN" | "en-US" +} + +export interface HttpProfile { + /** + * 请求方法 + * @type {"POST" | "GET"} + * 非必选 + */ + reqMethod?: "POST" | "GET" + /** + * 接入点域名,形如(cvm.ap-shanghai.tencentcloud.com) + * @type {string} + * 非必选 + */ + endpoint?: string + /** + * 协议,目前支持(https://) + * @type {string} + * 非必选 + */ + protocol?: string + /** + * 请求超时时间,默认60s + * @type {number} + * 非必选 + */ + reqTimeout?: number + /** + * 自定义请求头,例如 { "X-TC-TraceId": "ffe0c072-8a5d-4e17-8887-a8a60252abca" } + * @type {Record} + * 非必选 + */ + headers?: Record + /** + * 高级请求代理,例如 new HttpsProxyAgent("http://127.0.0.1:8899") + * + * 优先级高于 proxy 配置 + */ + agent?: Agent + /** + * http请求代理,例如 "http://127.0.0.1:8899" + */ + proxy?: string +} + +/** + * ClientProfile.language 属性支持的取值列表 + */ +export const SUPPORT_LANGUAGE_LIST = ["zh-CN", "en-US"] + +/** + * 认证信息类 + */ +export interface Credential { + /** + * 腾讯云账户secretId,secretKey + * 非必选,和 token 二选一 + */ + secretId?: string + /** + * 腾讯云账户secretKey + * 非必选,和 token 二选一 + */ + secretKey?: string + /** + * 腾讯云账户token + * 非必选,和 secretId 二选一 + */ + token?: string +} + +/** + * 动态认证信息 + */ +export interface DynamicCredential { + getCredential(): Promise +} diff --git a/src/main/domain/ddns/tx/sdk_version.ts b/src/main/domain/ddns/tx/sdk_version.ts new file mode 100644 index 0000000..2bc71af --- /dev/null +++ b/src/main/domain/ddns/tx/sdk_version.ts @@ -0,0 +1 @@ +export const sdkVersion = "4.0.885" diff --git a/src/main/domain/ddns/tx/sign.ts b/src/main/domain/ddns/tx/sign.ts new file mode 100644 index 0000000..87da5ad --- /dev/null +++ b/src/main/domain/ddns/tx/sign.ts @@ -0,0 +1,149 @@ +import TencentCloudSDKHttpException from "./exception/tencent_cloud_sdk_exception" +import * as crypto from "crypto" +import { URL } from "url" +// import * as JSONBigInt from "json-bigint" + +// const JSONbigNative = JSONBigInt({ useNativeBigInt: true }) + +/** + * @inner + */ +export default class Sign { + static sign(secretKey: string, signStr: string, signMethod: string): string { + const signMethodMap: { + [key: string]: string + } = { + HmacSHA1: "sha1", + HmacSHA256: "sha256", + } + + if (!signMethodMap.hasOwnProperty(signMethod)) { + throw new TencentCloudSDKHttpException( + "signMethod invalid, signMethod only support (HmacSHA1, HmacSHA256)" + ) + } + const hmac = crypto.createHmac(signMethodMap[signMethod], secretKey || "") + return hmac.update(Buffer.from(signStr, "utf8")).digest("base64") + } + + static sign3({ + method = "POST", + url = "", + payload, + timestamp, + service, + secretId, + secretKey, + multipart, + boundary, + headers: configHeaders = {}, + }: { + method?: string + url?: string + payload: any + timestamp: number + service: string + secretId: string + secretKey: string + multipart: boolean + boundary: string + headers: Record + }): string { + const urlObj = new URL(url) + const contentType = configHeaders["Content-Type"] + + // 通用头部 + let headers = "" + let signedHeaders = "" + if (method === "GET") { + signedHeaders = "content-type" + headers = `content-type:${contentType}\n` + } else if (method === "POST") { + signedHeaders = "content-type" + if (multipart) { + headers = `content-type:multipart/form-data; boundary=${boundary}\n` + } else { + headers = `content-type:${contentType}\n` + } + } + headers += `host:${urlObj.hostname}\n` + signedHeaders += ";host" + + const path = urlObj.pathname + const querystring = urlObj.search.slice(1) + + let payload_hash = "" + if (multipart) { + const hash = crypto.createHash("sha256") + hash.update(`--${boundary}`) + for (const key in payload) { + const content = payload[key] + if (Buffer.isBuffer(content)) { + hash.update( + `\r\nContent-Disposition: form-data; name="${key}"\r\nContent-Type: application/octet-stream\r\n\r\n` + ) + hash.update(content) + hash.update("\r\n") + } else if (typeof content === "string") { + hash.update(`\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n`) + hash.update(`${content}\r\n`) + } + hash.update(`--${boundary}`) + } + hash.update(`--\r\n`) + payload_hash = hash.digest("hex") + } else { + // const hashMessage = Buffer.isBuffer(payload) ? payload : JSONbigNative.stringify(payload) + const hashMessage = Buffer.isBuffer(payload) ? payload : JSON.stringify(payload) + payload_hash = payload ? getHash(hashMessage) : getHash("") + } + + const canonicalRequest = + method + + "\n" + + path + + "\n" + + querystring + + "\n" + + headers + + "\n" + + signedHeaders + + "\n" + + payload_hash + const date = getDate(timestamp) + + const StringToSign = + "TC3-HMAC-SHA256" + + "\n" + + timestamp + + "\n" + + `${date}/${service}/tc3_request` + + "\n" + + getHash(canonicalRequest) + + const kDate = sha256(date, "TC3" + secretKey) + const kService = sha256(service, kDate) + const kSigning = sha256("tc3_request", kService) + const signature = sha256(StringToSign, kSigning, "hex") + + return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${service}/tc3_request, SignedHeaders=${signedHeaders}, Signature=${signature}` + } +} + +function sha256(message: string, secret = "", encoding?: string): string { + const hmac = crypto.createHmac("sha256", secret) + return hmac.update(message).digest(encoding as any) +} + +function getHash(message: crypto.BinaryLike, encoding = "hex"): string { + const hash = crypto.createHash("sha256") + return hash.update(message).digest(encoding as any) +} + +function getDate(timestamp: number): string { + const date = new Date(timestamp * 1000) + const year = date.getUTCFullYear() + const month = ("0" + (date.getUTCMonth() + 1)).slice(-2) + const day = ("0" + date.getUTCDate()).slice(-2) + return `${year}-${month}-${day}` +} diff --git a/src/main/domain/ddns/tx/sse_response_model.ts b/src/main/domain/ddns/tx/sse_response_model.ts new file mode 100644 index 0000000..796da11 --- /dev/null +++ b/src/main/domain/ddns/tx/sse_response_model.ts @@ -0,0 +1,121 @@ +import { EventEmitter } from "events" +import { Readable } from "stream" + +interface EventSourceMessage { + /** The event ID to set the EventSource object's last event ID value. */ + id: string + /** A string identifying the type of event described. */ + event: string + /** The event data */ + data: string + /** The reconnection interval (in milliseconds) to wait before retrying the connection */ + retry?: number +} + +class SSEEventEmitter extends EventEmitter {} + +export class SSEResponseModel { + private stream: NodeJS.ReadableStream + private eventSource: SSEEventEmitter + + constructor(stream: NodeJS.ReadableStream) { + this.stream = stream + this.eventSource = new SSEEventEmitter() + this.init() + } + + /** + * @inner + */ + private init() { + const { stream, eventSource } = this + stream.on("data", (chunk) => { + if (chunk !== null) { + const messages = chunk.toString().split("\n\n") + for (let i = 0; i < messages.length; i++) { + if (messages[i].length > 0) { + eventSource.emit("message", this.parseSSEMessage(messages[i])) + } + } + } + }) + stream.on("close", () => { + eventSource.emit("close") + }) + stream.on("error", (err) => { + eventSource.emit("error", err) + }) + } + + /** + * @inner + */ + private parseSSEMessage(chunk: string) { + const message: EventSourceMessage = { + data: "", + event: "", + id: "", + retry: undefined, + } + + const lines = chunk.split("\n") + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + // line is of format ":" or ": " + const colonIndex = line.indexOf(":") + if (colonIndex <= 0) continue // exclude comments and lines with no values + const field = line.slice(0, colonIndex) + const value = line.slice(colonIndex + (line[colonIndex + 1] === " " ? 2 : 1)) + + switch (field) { + case "data": + message.data = message.data ? message.data + "\n" + value : value + break + case "event": + message.event = value + break + case "id": + message.id = value + break + case "retry": + const retry = parseInt(value, 10) + if (!isNaN(retry)) { + // per spec, ignore non-integers + message.retry = retry + } + break + } + } + + return message + } + + on(event: "message", listener: (message: EventSourceMessage) => void): this + on(event: "close", listener: () => void): this + on(event: "error", listener: (err: Error) => void): this + on(event: string, listener: any) { + this.eventSource.on(event, listener) + return this + } + + removeListener(event: "message", listener: (message: EventSourceMessage) => void): this + removeListener(event: "close", listener: () => void): this + removeListener(event: "error", listener: (err: Error) => void): this + removeListener(event: string, listener: any) { + this.eventSource.removeListener(event, listener) + return this + } + + async *[Symbol.asyncIterator](): AsyncIterableIterator { + for await (const chunk of this.stream) { + if (chunk !== null) { + const messages = chunk.toString().split("\n\n") + for (let i = 0; i < messages.length; i++) { + if (messages[i].length > 0) { + yield this.parseSSEMessage(messages[i]) + } + } + } + } + } +} diff --git a/src/main/domain/setting/setting.service.ts b/src/main/domain/setting/setting.service.ts index e6ab730..6f87c86 100644 --- a/src/main/domain/setting/setting.service.ts +++ b/src/main/domain/setting/setting.service.ts @@ -7,7 +7,6 @@ import {AuthFail, Sucess} from "../../other/Result"; import {ServerEvent} from "../../other/config"; import {FileSettingItem, SysSoftware, SysSoftwareItem, TokenTimeMode} from "../../../common/req/setting.req"; import {Env} from "../../../common/Env"; -import {ba} from "tencentcloud-sdk-nodejs"; import {SystemUtil} from "../sys/sys.utl"; import {Body} from "routing-controllers"; diff --git a/src/main/domain/sys/sys.service.ts b/src/main/domain/sys/sys.service.ts index ebb2ee9..e99e96f 100644 --- a/src/main/domain/sys/sys.service.ts +++ b/src/main/domain/sys/sys.service.ts @@ -14,7 +14,8 @@ export class SysService { cpu_brand: cpu.brand, cpu_core_num: cpu.cores, cpu_phy_core_num: cpu.physicalCores, - cpu_speed_hz: cpu.speedMax + cpu_speed_hz: cpu.speedMax, + pid_ppid:`${process.pid};${process.ppid}`, }) } diff --git a/src/main/server.ts b/src/main/server.ts index c2b0707..ab767fe 100644 --- a/src/main/server.ts +++ b/src/main/server.ts @@ -49,7 +49,12 @@ async function start() { routePrefix: '/api', classTransformer: true, // controllers: [`${__dirname}/domain/**/*.*s`], - controllers: [UserController, SysController, ShellController, FileController, DdnsController, NetController, NavindexController, SettingController, SSHController, RdpController, VideoController, CryptoController], + controllers:[ + UserController, SysController, ShellController, FileController, DdnsController, NetController, + NavindexController, SettingController, SSHController, RdpController, VideoController, CryptoController + ], + // controllers: [UserController, SysController, ShellController, FileController, DdnsController, NetController, + // NavindexController, SettingController, SSHController, RdpController, VideoController, CryptoController], // middlewares: [`${__dirname}/other/middleware/**/*.*s`], middlewares: [AuthMiddleware, GlobalErrorHandler], defaultErrorHandler: false, // 有自己的错误处理程序再禁用默认错误处理 diff --git a/src/web/project/component/sys/Sys.tsx b/src/web/project/component/sys/Sys.tsx index fa96639..7e0d84c 100644 --- a/src/web/project/component/sys/Sys.tsx +++ b/src/web/project/component/sys/Sys.tsx @@ -200,6 +200,7 @@ export function Sys(props) { + } {disk &&