@ghini/kit
Version:
js practical tools to assist efficient development
532 lines (481 loc) • 18.5 kB
JavaScript
// cf.js cloudflare使用api的便捷封装 - 兼容Global API Key和API Token
// 使用了每秒100次10ms间隔的高并发,不适合持续自行把控,cf限制1200次/5min
export { cf };
import { req } from "./http/req.js";
import { queue } from "./queue.js";
async function cf(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,
add,
madd,
set,
mset,
del,
mdel,
setSecurity, //开盾之类的操作
setByContent,
msetByContent,
setByContentForce,
msetByContentForce,
};
}
// --- 配置常量 ---
const CONFIG = {
MAX_RETRIES: 3, // 最大重试次数
RETRY_DELAY: 1000, // 初始重试延迟(毫秒)
};
// --- 核心辅助函数 ---
/**
* 重试机制 - 处理网络不稳定情况 (No changes needed)
*/
async function retryOperation(
fn,
maxRetries = CONFIG.MAX_RETRIES,
delay = CONFIG.RETRY_DELAY,
operation = "操作"
) {
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(
`'${operation}' 第 ${i + 1} 次失败,${retryDelay}ms 后重试...`
);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}
console.error(`'${operation}' 在 ${maxRetries} 次尝试后失败`);
throw lastError;
}
/**
* [REFACTORED] 统一的批量处理器,使用您的queue函数
* @param {Array} items - The items to process.
* @param {Function} processor - The async function to process each item.
* @param {Object} options - Configuration options.
* @param {Function} [options.groupBy=null] - A function to group items by a key to process them serially.
* @param {string} [options.operationName="批量操作"] - Name for the summary log.
* @returns {Promise<Array>}
*/
async function batchProcess(items, processor, options = {}) {
const { groupBy = null, operationName = "批量操作" } = options;
let results = new Array(items.length);
// 实例化您的队列
const runInQueue = queue(100, { minInterval: 10 });
if (groupBy) {
// --- Grouped Operations ---
// Safely process items for the same resource serially,
// while processing different groups concurrently via the queue.
const grouped = new Map();
items.forEach((item, index) => {
const key = groupBy(item);
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push({ item, index });
});
const groupPromises = Array.from(grouped.values()).map(group =>
runInQueue(async () => {
for (const { item, index } of group) {
try {
results[index] = await processor(item);
} catch (error) {
results[index] = { success: false, error: error.message, changed: false };
}
}
})
);
await Promise.all(groupPromises);
} else {
// --- Ungrouped Operations ---
// Process all items concurrently through the queue.
const promises = items.map((item, index) =>
runInQueue(async () => {
try {
results[index] = await processor(item);
} catch (error) {
results[index] = { success: false, error: error.message, changed: false };
}
})
);
await Promise.all(promises);
}
// --- 统一的、精简的日志总结 ---
const changedCount = results.filter((r) => r && r.changed).length;
const successCount = results.filter((r) => r && r.success).length;
const failCount = items.length - successCount;
console.log(`\n📊 ${operationName} 执行完成:`);
console.log(`✅ 变更: ${changedCount} 条`);
if (failCount > 0) {
console.log(`❌ 失败/跳过: ${failCount} 条`);
}
console.log(`📋 总计: ${items.length} 条`);
return results;
}
// --- 批量方法 (m*) ---
async function mset(arr) {
return batchProcess.call(this, arr, (item) => this.set(item), {
groupBy: (item) => (Array.isArray(item) ? item[0] : item.split(" ")[0]),
operationName: "mset",
});
}
async function madd(arr) {
return batchProcess.call(this, arr, (item) => this.add(item), {
groupBy: (item) => item.name,
operationName: "madd",
});
}
async function mdel(arr) {
return batchProcess.call(this, arr, (pre) => this.del(pre), {
groupBy: (pre) => pre,
operationName: "mdel",
});
}
async function msetByContent(updates) {
return batchProcess.call(this, updates, (update) => {
const [pre, oldContent, newContent, type, ttl] = update;
return this.setByContent(pre, oldContent, newContent, type, ttl);
}, {
groupBy: (update) => update[0],
operationName: "msetByContent",
}
);
}
async function msetByContentForce(updates) {
return batchProcess.call(this, updates, (update) => {
const [pre, oldContent, newContent, type, ttl] = update;
return this.setByContentForce(pre, oldContent, newContent, type, ttl);
}, {
groupBy: (update) => update[0],
operationName: "msetByContentForce",
}
);
}
// --- 核心单操作方法 ---
/**
* [REFINED] 获取Zone ID
*/
async function getZoneId() {
try {
const res = await retryOperation(
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 });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `获取Zone ID for ${this.domain}`
);
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;
}
}
/**
* [REFINED] 添加DNS记录
*/
async function add(json) {
try {
const res = await retryOperation(
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 });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `添加记录 ${json.name}`
);
if (res.data.success) {
console.log(`✅ 成功添加: ${json.name} ${json.content} ${json.type}`);
return { ...res.data, changed: true };
}
return { ...res.data, changed: false };
} catch (error) {
console.error(`添加记录 ${json.name} 失败:`, error.message);
throw error;
}
}
/**
* [REFINED] 删除指定前缀的所有A记录
*/
async function del(pre) {
const host = pre + "." + this.domain;
try {
const res = await retryOperation(
async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records?name=${host}`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `查询记录 ${host}`
);
const recordsToDelete = res.data.result || [];
if (recordsToDelete.length === 0) {
return { success: true, message: `记录 ${host} 不存在`, changed: false };
}
const runInQueue = queue(10, { minInterval: 100 });
const deletePromises = recordsToDelete.map(record =>
runInQueue(async () => {
await retryOperation(
async () => {
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
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `删除记录 ${host} (${record.content})`
);
})
);
await Promise.all(deletePromises);
console.log(`✅ 成功删除: ${host} (${recordsToDelete.length} 条)`);
return { success: true, changed: true };
} catch (error) {
console.error(`删除记录 ${host} 失败:`, error.message);
throw error;
}
}
/**
* [FINAL VERSION] 设置/更新DNS记录
*/
async function set(str) {
if (typeof str === "string") {
str = str.trim().replace(/ +/g, " ").split(" ");
}
let [pre, content, type, priority, ttl] = str;
const host = pre + "." + this.domain;
content = content || "";
type = type || "A";
priority = parseInt(priority) || 10;
ttl = parseInt(ttl) || 60;
if (ttl > 1 && ttl < 60) ttl = 60;
try {
if (!this.zid) throw new Error(`无法获取Zone ID,请检查域名: ${this.domain}`);
const desiredRecords = [];
if ((type === "A" || type === "AAAA") && content.includes(",")) {
const ipList = [ ...new Set( content.split(",").map((ip) => ip.trim()).filter((ip) => ip !== "") ), ];
ipList.forEach((ip) => {
desiredRecords.push({ type, name: host, content: ip, proxied: false, priority, ttl });
});
} else {
desiredRecords.push({ type, name: host, content, proxied: false, priority, ttl });
}
const res = await retryOperation( async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records?type=${type}&name=${host}`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `查询 ${host} 的现有记录`
);
const existingRecords = res.data.result || [];
const existingContents = new Set(existingRecords.map(r => r.content));
const desiredContents = new Set(desiredRecords.map(r => r.content));
const recordsToActuallyAdd = desiredRecords.filter( r => !existingContents.has(r.content) );
const recordsToActuallyDelete = existingRecords.filter( r => !desiredContents.has(r.content) );
const isChanged = recordsToActuallyAdd.length > 0 || recordsToActuallyDelete.length > 0;
if (!isChanged) {
return { success: true, changed: false, message: "记录无变化" };
}
const runInQueue = queue(10, { minInterval: 100 });
if (recordsToActuallyAdd.length > 0) {
const addPromises = recordsToActuallyAdd.map(record =>
runInQueue(() =>
retryOperation( () => add.bind({ auth: this.auth, headers: this.headers, zid: this.zid, set: true })(record),
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `添加记录 ${record.content}`
)
)
);
await Promise.all(addPromises);
}
if (recordsToActuallyDelete.length > 0) {
const deletePromises = recordsToActuallyDelete.map(record =>
runInQueue(async () => {
try {
await retryOperation( async () => {
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
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `删除记录 ${record.id} (${record.content})`
);
} catch (error) {
console.warn(`[!] 删除旧记录失败 (ID: ${record.id}):`, error.message);
}
})
);
await Promise.allSettled(deletePromises);
}
const message = `已将 ${host} 设置为 ${desiredContents.size} 条记录`;
if (desiredRecords.length > 1) {
const ips = desiredRecords.map((r) => r.content).join(", ");
console.log(`✅ 成功设置: ${host} ${type} → [${ips}]`);
} else {
const oldContents = existingRecords.map((r) => r.content).join(", ");
if (existingRecords.length > 0) {
console.log(`✅ 成功更新: ${host} ${type} [${oldContents}] → ${content}`);
} else {
console.log(`✅ 成功添加: ${host} ${type} ${content}`);
}
}
return { success: true, changed: true, message };
} catch (error) {
console.error(`[!] 操作 ${host} 时出错:`, error.message);
return { success: false, changed: false, error: error.message };
}
}
/**
* [REFINED] 根据内容查找并更新记录
*/
async function setByContent(pre, oldContent, newContent, type = "A", ttl = 60) {
const host = pre + "." + this.domain;
try {
if (!this.zid) throw new Error(`无法获取Zone ID`);
const res = await retryOperation( async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records?type=${type}&name=${host}`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, {}, this.headers)
: await req(reqUrl, { auth: this.auth });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `查询记录 ${host}`
);
if (!res.data.success) throw new Error(`查询记录失败`);
const targetRecord = res.data.result.find(r => r.content === oldContent);
if (!targetRecord) {
return { success: false, message: `未找到内容为 ${oldContent} 的记录`, action: "not_found", changed: false };
}
const updateData = { type, name: host, content: newContent, proxied: targetRecord.proxied, ttl };
const updateRes = await retryOperation( async () => {
const reqUrl = `https://api.cloudflare.com/client/v4/zones/${this.zid}/dns_records/${targetRecord.id} put`;
return this.headers && Object.keys(this.headers).length > 0
? await req(reqUrl, { json: updateData }, this.headers)
: await req(reqUrl, { auth: this.auth, json: updateData });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, `更新记录 ${host}`
);
if (updateRes.data.success) {
console.log(`✅ 成功更新: ${host} ${oldContent} → ${newContent}`);
return { success: true, message: `已更新`, action: "updated", changed: true };
} else {
throw new Error(`更新记录失败`);
}
} catch (error) {
console.error(`[!] 更新记录 ${host} 时出错:`, error.message);
return { success: false, error: error.message, changed: false };
}
}
/**
* [REFINED] 根据内容强制设置记录
*/
async function setByContentForce( pre, oldContent, newContent, type = "A", ttl = 60 ) {
const result = await this.setByContent( pre, oldContent, newContent, type, ttl );
if (result.success) {
return result;
}
// 如果未找到,则强制添加
return this.add({
type: type,
name: pre + "." + this.domain,
content: newContent,
proxied: false,
priority: 10,
ttl: ttl,
});
}
/**
* [REFINED] 设置安全规则 (WAF)
*/
async function setSecurity(options = {}) {
const { description = "安全规则", expression = "", action = "managed_challenge", priority = 999 } = options;
try {
const listResponse = await retryOperation( 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 });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, "查询安全规则"
);
const existingRule = listResponse.data.result.find( rule => rule.description === description );
if (existingRule) {
// 更新
const filterId = existingRule.filter.id;
await retryOperation( 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 });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, "更新过滤器"
);
const updateRes = await retryOperation( 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 });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, "更新安全规则"
);
console.log(`✅ 安全规则 "${description}" 更新成功!`);
return updateRes.data.result;
} else {
// 创建
const requestBody = [{ filter: { expression }, action, priority, description }];
const createRes = await retryOperation( 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 });
},
CONFIG.MAX_RETRIES, CONFIG.RETRY_DELAY, "创建安全规则"
);
console.log(`✅ 安全规则 "${description}" 创建成功!`);
return createRes.data.result[0];
}
} catch (error) {
console.error(`[!] 设置安全规则 "${description}" 时出错:`, error.message);
throw error;
}
}