This commit is contained in:
2025-11-14 20:43:59 +08:00
parent d1ba8bbab1
commit e35712820f
16 changed files with 606 additions and 14 deletions

0
.env.example Normal file
View File

6
.gitignore vendored
View File

@@ -6,4 +6,8 @@ dist
.remote-assets
components.d.ts
.slides.md.swp
.slides.md.swp
.env
!.env*example

3
agent/app.ts Normal file
View File

@@ -0,0 +1,3 @@
import { App } from '@kevisual/router'
export const app = new App();

View File

@@ -0,0 +1,37 @@
interface DnsUpdate {
zone_id: string;
record_id: string;
domain: string;
new_ip: string;
api_token: string;
type?: string; // 'A' or 'AAAA'
}
export class CloudflareDDNS {
makeHeader(api_token: string) {
return {
'Authorization': `Bearer ${api_token}`,
'Content-Type': 'application/json',
};
}
async updateRecord(data: DnsUpdate) {
const { zone_id, record_id, domain, type ,new_ip, api_token } = data;
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}`;
const body = {
"type": type || 'A',
"name": domain,
"content": new_ip,
"ttl": 0,
"proxied": false,
}
const response = await fetch(url, {
method: 'PUT',
headers: this.makeHeader(api_token),
body: JSON.stringify(body),
});
const result = await response.json();
if(!result.success) {
throw new Error(`更新失败: ${JSON.stringify(result.errors)}`);
}
return result;
}
}

View File

@@ -0,0 +1,12 @@
// 文档地址 https://www.volcengine.com/docs/6758/155086
// 记录管理
// 1. 获取域名记录列表 ListRecords
// 2. 获取单个域名记录 QueryRecord
// 3. 添加域名记录 CreateRecord
// 4. 修改域名记录 UpdateRecord
// 5. 删除域名记录 DeleteRecord
export class VolcengineDDNS {
baseURL = 'https://dns.volcengineapi.com';
version = '2018-08-01'
}

View File

@@ -0,0 +1,250 @@
const Service = "DNS";
const Version = "2018-08-01";
const Region = "cn-north-1";
const Host = "dns.volcengineapi.com";
interface Credential {
access_key_id: string;
secret_access_key: string;
service: string;
region: string;
}
interface RequestParam {
body: string;
host: string;
path: string;
method: string;
content_type: string;
date: Date;
query: Record<string, any>;
}
interface SignResult {
Host: string;
'X-Content-Sha256': string;
'X-Date': string;
'Content-Type': string;
Authorization?: string;
}
/**
* 字符串转ArrayBuffer
*/
function stringToArrayBuffer(str: string): ArrayBuffer {
const encoder = new TextEncoder();
return encoder.encode(str).buffer;
}
/**
* ArrayBuffer转十六进制字符串
*/
function arrayBufferToHex(buffer: ArrayBuffer): string {
const hashArray = Array.from(new Uint8Array(buffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
/**
* URL编码函数类似Python的quote函数
*/
function encodeURIComponentSafe(str: string): string {
return encodeURIComponent(str)
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
.replace(/%20/g, '%20');
}
/**
* 规范化查询参数
*/
function normQuery(params: Record<string, any>): string {
let query = "";
const sortedKeys = Object.keys(params).sort();
for (const key of sortedKeys) {
if (Array.isArray(params[key])) {
for (const k of params[key]) {
query += encodeURIComponentSafe(key) + "=" + encodeURIComponentSafe(k) + "&";
}
} else {
query += encodeURIComponentSafe(key) + "=" + encodeURIComponentSafe(params[key]) + "&";
}
}
query = query.slice(0, -1);
return query.replace(/\+/g, "%20");
}
/**
* HMAC-SHA256 加密 (使用Web Crypto API)
*/
async function hmacSha256(key: ArrayBuffer, content: string): Promise<ArrayBuffer> {
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
return crypto.subtle.sign('HMAC', cryptoKey, stringToArrayBuffer(content));
}
/**
* SHA256 哈希算法 (使用Web Crypto API)
*/
async function hashSha256(content: string): Promise<string> {
const hashBuffer = await crypto.subtle.digest('SHA-256', stringToArrayBuffer(content));
return arrayBufferToHex(hashBuffer);
}
/**
* 获取签名
*/
export async function getSignature(
method: string,
query: Record<string, any>,
header: Record<string, string>,
ak: string,
sk: string,
action: string,
body: any
): Promise<SignResult> {
const credential: Credential = {
access_key_id: ak,
secret_access_key: sk,
service: Service,
region: Region,
};
const fullQuery = { Action: action, Version: Version, ...query };
const sortedQuery: Record<string, any> = {};
Object.keys(fullQuery).sort().forEach(key => {
sortedQuery[key] = (fullQuery as any)[key];
});
const requestParam: RequestParam = {
body: "",
host: Host,
path: "/",
method: method,
content_type: "application/json",
date: new Date(),
query: sortedQuery,
};
if (method === "POST") {
requestParam.body = JSON.stringify(body);
}
const xDate = requestParam.date.toISOString().replace(/[:\-]|\.\d{3}/g, '').slice(0, -1) + 'Z';
const shortXDate = xDate.slice(0, 8);
const xContentSha256 = await hashSha256(requestParam.body);
const signResult: SignResult = {
Host: requestParam.host,
'X-Content-Sha256': xContentSha256,
'X-Date': xDate,
'Content-Type': requestParam.content_type,
};
const signedHeadersStr = ["content-type", "host", "x-content-sha256", "x-date"].join(";");
const canonicalRequestStr = [
requestParam.method,
requestParam.path,
normQuery(requestParam.query),
[
"content-type:" + requestParam.content_type,
"host:" + requestParam.host,
"x-content-sha256:" + xContentSha256,
"x-date:" + xDate,
].join("\n"),
"",
signedHeadersStr,
xContentSha256,
].join("\n");
const hashedCanonicalRequest = await hashSha256(canonicalRequestStr);
const credentialScope = [shortXDate, credential.region, credential.service, "request"].join("/");
const stringToSign = ["HMAC-SHA256", xDate, credentialScope, hashedCanonicalRequest].join("\n");
const secretKeyBuffer = stringToArrayBuffer(credential.secret_access_key);
const kDate = await hmacSha256(secretKeyBuffer, shortXDate);
const kRegion = await hmacSha256(kDate, credential.region);
const kService = await hmacSha256(kRegion, credential.service);
const kSigning = await hmacSha256(kService, "request");
const signatureBuffer = await hmacSha256(kSigning, stringToSign);
const signature = arrayBufferToHex(signatureBuffer);
signResult.Authorization = `HMAC-SHA256 Credential=${credential.access_key_id}/${credentialScope}, SignedHeaders=${signedHeadersStr}, Signature=${signature}`;
return signResult;
}
/**
* 发送API请求
*/
export async function request(
method: string,
query: Record<string, any>,
header: Record<string, string>,
ak: string,
sk: string,
action: string,
body: any
): Promise<any> {
const signResult = await getSignature(method, query, header, ak, sk, action, body);
const finalHeader = { ...header, ...signResult };
const requestParam: RequestParam = {
body: method === "POST" ? JSON.stringify(body) : "",
host: Host,
path: "/",
method: method,
content_type: "application/json",
date: new Date(),
query: { Action: action, Version: Version, ...query },
};
const url = new URL(`https://${requestParam.host}${requestParam.path}`);
Object.keys(requestParam.query).forEach(key => {
url.searchParams.append(key, requestParam.query[key]);
});
const fetchOptions: RequestInit = {
method: method,
headers: finalHeader,
};
if (method === "POST") {
fetchOptions.body = requestParam.body;
}
try {
const response = await fetch(url.toString(), fetchOptions);
return await response.json();
} catch (error) {
throw new Error(`Request failed: ${error}`);
}
}
/**
* 使用示例
*/
export async function example(ak: string, sk: string) {
try {
// POST 请求示例 - 调用 UpdateZone API
const requestBody = {
ZID: 100,
Remark: "example",
};
const updateZoneResult = await request("POST", {}, {}, ak, sk, "UpdateZone", requestBody);
console.log(updateZoneResult);
// GET 请求示例 - 调用 CheckZone API
const requestQuery = { ZoneName: "example.com" };
const checkZoneResult = await request("GET", requestQuery, {}, ak, sk, "CheckZone", {});
console.log(checkZoneResult);
} catch (error) {
console.error('API request failed:', error);
}
}

14
agent/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import {app} from './app.ts';
import './ip'
// app.listen(8080);
app.call({
path: 'ip',
key: 'v6'
}).then(res => {
console.log('IPv4 Address:', res);
})
export { app };

41
agent/ip.ts Normal file
View File

@@ -0,0 +1,41 @@
const baseURLv4 = 'https://4.ipw.cn/';
const baseURLv6 = 'https://6.ipw.cn/';
import { app } from './app.ts';
app.route({
path: 'ip',
key: 'v4',
description: '获取当前公网IPv4地址',
}).define(async (ctx) => {
const response = await fetch(baseURLv4);
const ip = (await response.text()).trim();
if(!isIpv4(ip)) {
ctx.throw?.('获取地址失败');
}
ctx.body = { ip };
}).addTo(app);
app.route({
path: 'ip',
key: 'v6',
description: '获取当前公网IPv6地址',
}).define(async (ctx) => {
const response = await fetch(baseURLv6);
const ip = (await response.text()).trim();
if(!isIpv6(ip)) {
ctx.throw?.('获取地址失败');
}
ctx.body = { ip };
}).addTo(app);
export const isIpv6 = (ip: string): boolean => {
return ip.includes(':');
}
export const isIpv4 = (ip: string): boolean => {
return ip.split('.').length === 4;
}

View File

@@ -0,0 +1,18 @@
{
"name": "@kevisual/slidev-template",
"type": "module",
"basename": "/root/slidev-template",
"version": "0.0.1",
"scripts": {
"build": "slidev build --base /root/slidev-template/",
"dev": "slidev --open",
"pub": "ev deploy ./dist -k slidev-template -v 0.0.1 -u",
"export": "slidev export"
},
"dependencies": {
"@slidev/cli": "^52.8.0",
"@slidev/theme-default": "latest",
"@slidev/theme-seriph": "latest",
"vue": "^3.5.24"
}
}

View File

@@ -1,18 +1,20 @@
{
"name": "@kevisual/slidev-template",
"type": "module",
"basename": "/root/slidev-template",
"version": "0.0.1",
"name": "@kevisual/ddns-agent",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "slidev build --base /root/slidev-template/",
"dev": "slidev --open",
"pub": "ev deploy ./dist -k slidev-template -v 0.0.1 -u",
"export": "slidev export"
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.19.0",
"dependencies": {
"@slidev/cli": "^52.8.0",
"@slidev/theme-default": "latest",
"@slidev/theme-seriph": "latest",
"vue": "^3.5.24"
"@kevisual/router": "^0.0.30",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@types/node": "^24.10.1"
}
}
}

211
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,211 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@kevisual/router':
specifier: ^0.0.30
version: 0.0.30
crypto-js:
specifier: ^4.2.0
version: 4.2.0
devDependencies:
'@types/node':
specifier: ^24.10.1
version: 24.10.1
packages:
'@kevisual/router@0.0.30':
resolution: {integrity: sha512-/mBo7aZFWjT4QfHkI5HPXfdgSwZzt3mAVei7dcNSBTPe9KQSoYKZ8BTq9VTUj3XE0sI6o1bZjlLYvinpVnZilw==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.1:
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
engines: {node: '>= 0.6'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
selfsigned@3.0.1:
resolution: {integrity: sha512-6U6w6kSLrM9Zxo0D7mC7QdGS6ZZytMWBnj/vhF9p+dAHx6CwGezuRcO4VclTbrrI7mg7SD6zNiqXUuBHOVopNQ==}
engines: {node: '>=10'}
send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
snapshots:
'@kevisual/router@0.0.30':
dependencies:
path-to-regexp: 8.3.0
selfsigned: 3.0.1
send: 1.2.0
transitivePeerDependencies:
- supports-color
'@types/node@24.10.1':
dependencies:
undici-types: 7.16.0
crypto-js@4.2.0: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
depd@2.0.0: {}
ee-first@1.1.1: {}
encodeurl@2.0.0: {}
escape-html@1.0.3: {}
etag@1.8.1: {}
fresh@2.0.0: {}
http-errors@2.0.0:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
inherits@2.0.4: {}
mime-db@1.54.0: {}
mime-types@3.0.1:
dependencies:
mime-db: 1.54.0
ms@2.1.3: {}
node-forge@1.3.1: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
path-to-regexp@8.3.0: {}
range-parser@1.2.1: {}
selfsigned@3.0.1:
dependencies:
node-forge: 1.3.1
send@1.2.0:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.0
mime-types: 3.0.1
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
setprototypeof@1.2.0: {}
statuses@2.0.1: {}
statuses@2.0.2: {}
toidentifier@1.0.1: {}
undici-types@7.16.0: {}