码桶

发现社区成员的开源项目

transform-rules.js5.9 KB
import { logAudit } from '../../_audit.js';

const PHASES = {
    url_rewrite: 'http_request_transform',
    header_mod: 'http_request_late_transform'
};

function entrypoint(zoneId, phase) {
    return `https://api.cloudflare.com/client/v4/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`;
}

async function fetchRuleset(cfHeaders, zoneId, phase) {
    try {
        const res = await fetch(entrypoint(zoneId, phase), {
            headers: { ...cfHeaders, 'Content-Type': 'application/json' }
        });
        const data = await res.json();
        if (data.success) return data.result;
        return null;
    } catch {
        return null;
    }
}

async function putRules(cfHeaders, zoneId, ruleset, rules, cfPhase) {
    const url = ruleset?.id
        ? `https://api.cloudflare.com/client/v4/zones/${zoneId}/rulesets/${ruleset.id}`
        : entrypoint(zoneId, cfPhase);
    const res = await fetch(url, {
        method: 'PUT',
        headers: { ...cfHeaders, 'Content-Type': 'application/json' },
        body: JSON.stringify({ rules })
    });
    return res.json();
}

export async function onRequestGet(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;
    try {
        const [urlRewrite, headerMod] = await Promise.all([
            fetchRuleset(cfHeaders, zoneId, PHASES.url_rewrite),
            fetchRuleset(cfHeaders, zoneId, PHASES.header_mod),
        ]);

        return new Response(JSON.stringify({
            success: true,
            url_rewrite_rules: urlRewrite?.rules || [],
            url_rewrite_ruleset_id: urlRewrite?.id || null,
            header_mod_rules: headerMod?.rules || [],
            header_mod_ruleset_id: headerMod?.id || null,
            errors: []
        }), { headers: { 'Content-Type': 'application/json' } });
    } catch (e) {
        return new Response(JSON.stringify({ success: false, errors: [{ message: e.message || 'Failed to fetch transform rules' }] }), { status: 500, headers: { 'Content-Type': 'application/json' } });
    }
}

export async function onRequestPost(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;
    const body = await context.request.json();
    const username = context.data.user?.username || 'client';
    const kv = context.env.CF_DNS_KV;
    const { action, phase } = body;
    const cfPhase = PHASES[phase];

    if (!cfPhase) return jsonErr(`Invalid phase: "${phase}"`);

    try {
        if (action === 'toggle_rule') {
            const { ruleIndex, enabled } = body;
            const ruleset = await fetchRuleset(cfHeaders, zoneId, cfPhase);
            if (!ruleset) return jsonErr('Could not fetch ruleset');
            const rules = ruleset.rules || [];
            if (ruleIndex < 0 || ruleIndex >= rules.length) return jsonErr('Invalid rule index');
            rules[ruleIndex] = { ...rules[ruleIndex], enabled };
            const putData = await putRules(cfHeaders, zoneId, ruleset, rules, cfPhase);
            if (putData.success) {
                await logAudit(kv, username, 'transform.toggle', `${enabled ? 'Enabled' : 'Disabled'} ${phase} rule #${ruleIndex + 1} (zone: ${zoneId})`);
            }
            return jsonRes(putData);
        }

        if (action === 'create_rule') {
            const { rule } = body;
            const ruleset = await fetchRuleset(cfHeaders, zoneId, cfPhase);
            const rules = ruleset?.rules || [];
            rules.push({ ...rule, action: rule.action || 'rewrite' });
            const putData = await putRules(cfHeaders, zoneId, ruleset, rules, cfPhase);
            if (putData.success) {
                await logAudit(kv, username, 'transform.create', `Created ${phase} rule "${rule.description || ''}" (zone: ${zoneId})`);
            }
            return jsonRes(putData);
        }

        if (action === 'update_rule') {
            const { ruleIndex, rule } = body;
            const ruleset = await fetchRuleset(cfHeaders, zoneId, cfPhase);
            if (!ruleset) return jsonErr('Could not fetch ruleset');
            const rules = ruleset.rules || [];
            if (ruleIndex < 0 || ruleIndex >= rules.length) return jsonErr('Invalid rule index');
            rules[ruleIndex] = { ...rules[ruleIndex], ...rule, action: rule.action || 'rewrite' };
            const putData = await putRules(cfHeaders, zoneId, ruleset, rules, cfPhase);
            if (putData.success) {
                await logAudit(kv, username, 'transform.update', `Updated ${phase} rule #${ruleIndex + 1} (zone: ${zoneId})`);
            }
            return jsonRes(putData);
        }

        if (action === 'delete_rule') {
            const { ruleIndex } = body;
            const ruleset = await fetchRuleset(cfHeaders, zoneId, cfPhase);
            if (!ruleset) return jsonErr('Could not fetch ruleset');
            const rules = ruleset.rules || [];
            if (ruleIndex < 0 || ruleIndex >= rules.length) return jsonErr('Invalid rule index');
            const deleted = rules.splice(ruleIndex, 1)[0];
            const putData = await putRules(cfHeaders, zoneId, ruleset, rules, cfPhase);
            if (putData.success) {
                await logAudit(kv, username, 'transform.delete', `Deleted ${phase} rule "${deleted.description || '#' + (ruleIndex + 1)}" (zone: ${zoneId})`);
            }
            return jsonRes(putData);
        }

        return jsonErr(`Unknown action: "${action}"`);
    } catch (e) {
        return new Response(JSON.stringify({ success: false, errors: [{ message: e.message }] }), { status: 500, headers: { 'Content-Type': 'application/json' } });
    }
}

function jsonRes(data) {
    return new Response(JSON.stringify({ success: data.success, errors: data.errors || [] }), {
        status: data.success ? 200 : 400, headers: { 'Content-Type': 'application/json' }
    });
}
function jsonErr(msg) {
    return new Response(JSON.stringify({ success: false, errors: [{ message: msg }] }), {
        status: 400, headers: { 'Content-Type': 'application/json' }
    });
}