@ghini/kit
Version:
js practical tools to assist efficient development
432 lines (427 loc) • 13.5 kB
JavaScript
// cf2.js 精简灵活的新版,采取换代逐步替代方案,不影响旧版使用
export { cf2 };
import { isipv4, isipv6, queue, req, reqdata } from "../main.js";
async function cf2(obj) {
const key = obj.key;
const domain = obj.domain;
const email = obj.email;
// 根据是否提供email决定认证方式
let auth,
headers = {};
if (email) {
// Global API Key认证方式
headers = {
"X-Auth-Email": email,
"X-Auth-Key": key,
};
} else {
// API Token认证方式
auth = "Bearer " + key;
}
// 调用时传入正确的参数
const zid = await getZoneId.bind({ domain, auth, headers })();
return {
auth,
headers,
domain,
zid,
getZoneId,
// 精确+一点魔法
dnsObj,
find,
add,
del,
set, //尚未完善
madd,
mdel,
mset,
// 特化
// 其他(安全规则)
security,
};
}
// --- 配置常量 ---
const CONFIG = {
MAX_RETRIES: 3, // 最大重试次数
RETRY_DELAY: 1000, // 初始重试延迟(毫秒)
};
const qrun = queue(100, { minInterval: 10 });
// --- 核心辅助函数 ---
/**
* 重试机制 - 处理网络不稳定情况 (No changes needed)
*/
async function retry(
fn,
maxRetries = CONFIG.MAX_RETRIES,
delay = CONFIG.RETRY_DELAY
) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (
error.message &&
(error.message.includes("权限不足") ||
error.message.includes("认证失败") ||
error.message.includes("Invalid API key") ||
error.message.includes("unauthorized"))
) {
throw error;
}
if (i < maxRetries - 1) {
const retryDelay = delay * Math.pow(2, i);
console.log(`第 ${i + 1} 次失败,${retryDelay}ms 后重试...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}
console.error(`在 ${maxRetries} 次尝试后失败`);
throw lastError;
}
/** 如果是字符串,默认根据名字找 */
async function find(filter) {
filter = this.dnsObj(filter, `find`);
const sp = new URLSearchParams(filter).toString();
// console.log(111, sp);
const reqUrl =
`https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records?` + sp;
let res = await retry(() =>
this.headers && Object.keys(this.headers).length > 0
? reqdata(reqUrl, {}, this.headers)
: reqdata(reqUrl, { auth: this.auth })
);
res = res.result;
if (filter) {
res = res.filter((v) => {
if (filter.type && v.type != filter.type) return;
if (filter.content && v.content != filter.content) return;
if (filter.proxiable && v.proxiable != filter.proxiable) return;
if (filter.proxied && v.proxied != filter.proxied) return;
if (filter.ttl && v.ttl != filter.ttl) return;
if (filter.comment && v.comment != filter.comment) return;
if (filter.tags && v.tags != filter.tags) return;
return 1;
});
}
res = res.map((v) => {
delete v.proxiable;
delete v.proxied;
delete v.ttl;
delete v.settings;
delete v.meta;
delete v.comment;
delete v.tags;
delete v.created_on;
delete v.modified_on;
return v;
});
return res;
}
/**
* 添加DNS记录,保持纯粹1对1,能智能识别A和AAAA
* @param {String|Array|Object} str
* @returns 添加数量
*/
async function add(str) {
const json = this.dnsObj(str);
const res = await retry(async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records post`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, { json }, this.headers)
: await req(reqUrl, { auth: this.auth, json });
});
if (res.data.success) return 1;
//81058: record already exists
if (res.data.errors[0].code != 81058)
console.error(`add失败 "${json.name}" :`, res.data.errors[0]);
return 0;
}
/**
* 删除指定前缀的所有A记录,需要先查出来,再根据id删除
* @returns 删除的数组
*/
async function del(filter) {
if (typeof filter === "object" && !filter.name && !filter.content) {
console.warn("删除必须有name或content才能安全执行");
return [];
}
let res = await this.find(filter);
const del_arr = res.map((v) => {
return {
name: v.name,
type: v.type,
content: v.content,
};
});
if (res.length === 0) return [];
res = await Promise.all(
res.map((record) =>
qrun(() =>
retry(() => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records/${record.id} delete`;
return this.headers && Object.keys(this.headers).length > 0
? reqdata(reqUrl, this.headers)
: reqdata(reqUrl, { auth: this.auth });
})
)
)
);
// 涉及到删除,还是打印出来好
console.warn(
del_arr.map((v) => v.name + " " + v.type + " " + v.content),
res.length,
`发生记录删除cf.del`
);
return del_arr;
}
/**
* 智能处理dns参数为标准规格
* @param {*} dnsParam
* @param {*} option 对于复杂的情况,用option分别
* @returns
*/
function dnsObj(dnsParam, option = "") {
// 多种输入类型处理
let name, content, type, priority, proxied, ttl;
if (typeof dnsParam === "string") {
dnsParam = dnsParam.trim().replace(/ +/g, " ").split(" ");
}
if (Array.isArray(dnsParam)) {
[name, content, type, priority, proxied, ttl] = dnsParam;
} else {
({ name, content, type, priority, proxied, ttl } = dnsParam);
}
if (option === "set") {
if (!content) {
content = name;
name = "";
}
if (!type) {
if (isipv4(content)) type = "A";
else if (isipv6(content)) type = "AAAA";
else {
type = "TXT";
if (content[0] != '"') content = `"` + content;
if (content.slice(-1) != `"`) content += `"`;
}
} else type = type.toUpperCase();
option = "find";
}
if (name && !name.includes("." + this.domain))
name = name + "." + this.domain;
if (option === "find") {
// 不默认参数值
const tmp = {};
if (name) tmp.name = name;
if (content) tmp.content = content;
if (type) tmp.type = type.toUpperCase();
if (priority || priority === 0) tmp.priority = priority;
if (proxied) tmp.proxied = true;
if (ttl) tmp.ttl = ttl;
dnsParam = tmp;
} else {
if (!type) {
if (isipv4(content)) type = "A";
else if (isipv6(content)) type = "AAAA";
else {
type = "TXT";
if (content[0] != '"') content = `"` + content;
if (content.slice(-1) != `"`) content += `"`;
}
} else type = type.toUpperCase();
priority = parseInt(priority) || 10;
proxied = proxied ? true : false;
ttl = parseInt(ttl) || 60;
dnsParam = {
name,
content,
type,
priority, //MX和SRV 0-65535 默认给10(主要邮件系统用)
proxied, //小黄云也挺少用到的
ttl, //一般不用设置 60s有利无弊 cf也完全吃得消 有特殊需要依旧可以设置
};
}
// console.log(`dnsObj`, option, dnsParam);
return dnsParam;
}
/**
* set有两个参数是合理且应该的,混合在一起会为本就复杂的情况复杂加倍。
* set本质就是del+add 此函数复杂度高(因魔法重载处理),需要在实战中多多测试稳定性和推演
* 为筛选出的内容,添加目标content,多对1;多对多(A AAAA)
* 跟del异曲同工,找出所有符合条件的,进行覆盖设置,若没有则直接添加
* 干净利落,会把匹配到的全删掉,然后添加新内容
* @param {*} filter 用来选择目标删除
* @param {*} content 待修改的内容
* @returns 成功修改的数量
*/
async function set(filter, json) {
// filter如果没有type继承json识别的type,json如果没有name继承filter的name
filter = this.dnsObj(filter, "find");
json = this.dnsObj(json, "set");
if (!filter.type) filter.type = json.type;
if (!json.name) json.name = filter.name;
// console.log(filter);
// console.log(json);
let res = await this.del(filter);
// console.log(res);
if (!json.name) {
if (res.length === 0) return 0;
return (
await Promise.all(
res.map((v) => this.add({ ...json, ...{ name: v.name } }))
)
).reduce((pre, cur) => pre + cur, 0);
}
return this.add(json);
}
async function mset(arr) {
const grouped = new Map();
arr.forEach((item, index) => {
const key = Array.isArray(item) ? item[0] : item.split(" ")[0];
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push({ item, index });
});
let results = new Array(arr.length);
const groupPromises = Array.from(grouped.values()).map((group) =>
qrun(async () => {
for (const { item, index } of group) {
try {
results[index] = await this.set(item);
} catch (error) {
results[index] = { success: false, error: error.message };
}
}
})
);
await Promise.all(groupPromises);
return results;
}
async function madd(arr) {
const grouped = new Map();
arr.forEach((item, index) => {
if (!grouped.has(item.name)) grouped.set(item.name, []);
grouped.get(item.name).push({ item, index });
});
let results = new Array(arr.length);
const groupPromises = Array.from(grouped.values()).map((group) =>
qrun(async () => {
for (const { item, index } of group) {
try {
results[index] = await this.add(item);
} catch (error) {
results[index] = { success: false, error: error.message };
}
}
})
);
await Promise.all(groupPromises);
return results;
}
async function mdel(arr) {
const grouped = new Map();
arr.forEach((pre, index) => {
if (!grouped.has(pre)) grouped.set(pre, []);
grouped.get(pre).push({ pre, index });
});
let results = new Array(arr.length);
const groupPromises = Array.from(grouped.values()).map((group) =>
qrun(async () => {
for (const { pre, index } of group) {
try {
results[index] = await this.del(pre);
} catch (error) {
results[index] = { success: false, error: error.message };
}
}
})
);
await Promise.all(groupPromises);
return results;
}
/**
* 获取Zone ID
*/
async function getZoneId() {
try {
const res = await retry(async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones?name=${this.domain}`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
});
if (res.data.success && res.data.result.length > 0) {
return res.data.result[0].id;
} else {
throw new Error("记录未找到或权限不足");
}
} catch (error) {
console.error("获取 Zone ID 失败:", error.message);
return null;
}
}
/**
* 设置安全规则 (WAF)
*/
async function security(options = {}) {
const {
description = "安全规则",
expression = "",
action = "managed_challenge",
priority = 999,
} = options;
try {
const listResponse = await retry(async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/firewall/rules`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
});
const existingRule = listResponse.data.result.find(
(rule) => rule.description === description
);
if (existingRule) {
// 更新
const filterId = existingRule.filter.id;
await retry(async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/filters/${filterId} put`;
const json = { expression, paused: false };
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, { json }, this.headers)
: await req(reqUrl, { auth: this.auth, json });
});
const updateRes = await retry(async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/firewall/rules/${existingRule.id} put`;
const json = {
action,
priority,
paused: false,
description,
filter: { id: filterId },
};
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, { json }, this.headers)
: await req(reqUrl, { auth: this.auth, json });
});
console.log(`✅ 安全规则 "${description}" 更新成功!`);
return updateRes.data.result;
} else {
// 创建
const requestBody = [
{ filter: { expression }, action, priority, description },
];
const createRes = await retry(async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/firewall/rules post`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, { json: requestBody }, this.headers)
: await req(reqUrl, { auth: this.auth, json: requestBody });
});
console.log(`✅ 安全规则 "${description}" 创建成功!`);
return createRes.data.result[0];
}
} catch (error) {
console.error(`[!] 设置安全规则 "${description}" 时出错:`, error.message);
throw error;
}
}