export default {
async fetch(request, env) {
try {
const url = new URL(request.url);
let path = url.pathname.toLowerCase();
if (path !== '/' && path.endsWith('/')) path = path.slice(0, -1);
if (path === '/api/verify-password') return handleVerifyPassword(request, env);
if (path === '/api/login') return handleLogin(request, env);
if (path === '/api/verify-token') return handleVerifyToken(request, env);
if (path === '/api/add-rule') return handleAddRule(request, env);
if (path === '/api/update-rule') return handleUpdateRule(request, env);
if (path === '/api/delete-rule') return handleDeleteRule(request, env);
if (path === '/api/update-redirect-site') return handleUpdateRedirectSite(request, env);
if (path === '/api/update-password') return handleUpdatePassword(request, env);
const { rules, siteConfig } = await loadRedirectData(env);
const defaultRoot = rules.find(r => r.shortPath === '/');
if (path === '/' && defaultRoot) {
await incrementRedirectCount(env);
return Response.redirect(defaultRoot.targetUrl, 307);
}
if (path === '/rules') {
const adminAuth = await getAdminAuth(env);
const redirectCount = await getRedirectCount(env);
const host = new URL(request.url).host;
return createRulesPage(rules, redirectCount, adminAuth.hasAdmin, siteConfig, host);
}
for (const rule of rules) {
if (rule.shortPath === '/') continue;
const paramRegex = /\/:([^/]+)/g;
const paramMatches = [...rule.shortPath.matchAll(paramRegex)];
if (paramMatches.length > 0) {
const regex = new RegExp(`^${rule.shortPath.replace(paramRegex, '/([^/]+)')}$`);
const m = path.match(regex);
if (m) {
let target = rule.targetUrl;
paramMatches.forEach((match, i) => { target = target.replace(`:${match[1]}`, m[i + 1]); });
await incrementRedirectCount(env);
return Response.redirect(target, 307);
}
}
if (path === rule.shortPath) {
await incrementRedirectCount(env);
return Response.redirect(rule.targetUrl, 307);
}
}
return createNotFoundResponse(request, path, siteConfig.notFoundUrl);
} catch (e) {
console.error('Worker错误:', e);
return new Response('服务器内部错误', { status: 500 });
}
}
};
// ========== 管理员认证 ==========
async function getAdminAuth(env) {
try {
const config = await env.KV_PAN.get('sys_config', { type: 'json' });
if (config && config.admin) return { password: config.admin, hasAdmin: true };
} catch (e) {}
if (env.ADMIN_PASSWORD) return { password: env.ADMIN_PASSWORD, hasAdmin: true };
return { password: null, hasAdmin: false };
}
async function verifyAdminPassword(pass, adminAuth) {
return pass && adminAuth.password && pass === adminAuth.password;
}
// ========== JWT 工具函数 ==========
async function generateJWT(secret) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const header = { alg: 'HS256', typ: 'JWT' };
const payload = {
role: 'admin',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 7200
};
const encodedHeader = btoa(JSON.stringify(header));
const encodedPayload = btoa(JSON.stringify(payload));
const unsigned = `${encodedHeader}.${encodedPayload}`;
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(unsigned));
const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signature)));
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
}
async function verifyJWT(token, secret) {
try {
const parts = token.split('.');
if (parts.length !== 3) return false;
const [headerB64, payloadB64, sigB64] = parts;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const unsigned = `${headerB64}.${payloadB64}`;
const signature = Uint8Array.from(atob(sigB64), c => c.charCodeAt(0));
const valid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(unsigned));
if (!valid) return false;
const payload = JSON.parse(atob(payloadB64));
return payload.exp > Math.floor(Date.now() / 1000);
} catch (e) {
return false;
}
}
async function requireAdmin(req, env) {
const auth = req.headers.get('Authorization');
if (!auth || !auth.startsWith('Bearer ')) return false;
const token = auth.slice(7);
return await verifyJWT(token, env.JWT_SECRET);
}
// ========== 数据层 ==========
async function loadRedirectData(env) {
const defaultSiteConfig = {
siteName: 'langaj.cn/rules',
favicon: 'https://www.langaj.cn/src/cloud.png',
navTitle: 'langaj.cn v1.3',
titleLarge: '快速访问服务',
titleSmall: '短链接快速访问服务全面升级,接入 Serverless 边缘计算,说明文档: langaj.cn/resd 。',
notFoundUrl: 'https://www.langaj.cn/404.html',
copyrightText: '2026 langaj.cn'
};
try {
const raw = await env.KV_PAN.get('redirect_data', { type: 'json' });
if (raw && Array.isArray(raw.rules)) {
return {
rules: raw.rules,
siteConfig: { ...defaultSiteConfig, ...raw.siteConfig }
};
}
} catch (e) {}
const initial = { rules: [], siteConfig: defaultSiteConfig };
await env.KV_PAN.put('redirect_data', JSON.stringify(initial));
return initial;
}
async function saveRedirectData(env, data) {
await env.KV_PAN.put('redirect_data', JSON.stringify(data));
}
async function getRedirectCount(env) {
const row = await env.DB.prepare('SELECT count FROM redirect_counter WHERE id = 1').first();
return row ? row.count : 0;
}
async function incrementRedirectCount(env) {
await env.DB.prepare('UPDATE redirect_counter SET count = count + 1 WHERE id = 1').run();
}
// ========== API 处理 ==========
async function handleVerifyPassword(req, env) {
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r400({ valid: false, error: '管理功能未启用' });
const pass = new URL(req.url).searchParams.get('pass');
return r200({ valid: await verifyAdminPassword(pass, adminAuth) });
}
async function handleLogin(req, env) {
if (req.method !== 'POST') return r405();
const { pass } = await req.json();
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r403('管理功能未启用');
if (!(await verifyAdminPassword(pass, adminAuth))) return r403('密码错误');
const token = await generateJWT(env.JWT_SECRET);
return r200({ token });
}
async function handleVerifyToken(req, env) {
if (req.method !== 'POST') return r405();
const valid = await requireAdmin(req, env);
return r200({ valid });
}
async function handleAddRule(req, env) {
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r403('管理功能未启用');
if (!(await requireAdmin(req, env))) return r403('未授权或令牌失效');
if (req.method !== 'POST') return r405();
let { shortPath, targetUrl, note } = await req.json();
if (!shortPath.startsWith('/')) shortPath = '/' + shortPath;
if (!/^https?:\/\//i.test(targetUrl)) targetUrl = 'https://' + targetUrl;
if (!shortPath || !targetUrl) return r400('缺少必填字段');
const data = await loadRedirectData(env);
if (data.rules.some(r => r.shortPath === shortPath)) return r409('短路径已存在');
data.rules.push({ shortPath, targetUrl, note: note || '' });
await saveRedirectData(env, data);
return r200({ success: true });
}
async function handleUpdateRule(req, env) {
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r403('管理功能未启用');
if (!(await requireAdmin(req, env))) return r403('未授权或令牌失效');
if (req.method !== 'PUT' && req.method !== 'POST') return r405();
const { originalShortPath, newShortPath, targetUrl, note } = await req.json();
if (!originalShortPath || !newShortPath || !targetUrl) return r400('缺少必填字段');
const data = await loadRedirectData(env);
const idx = data.rules.findIndex(r => r.shortPath === originalShortPath);
if (idx === -1) return r404('未找到原规则');
if (newShortPath !== originalShortPath && data.rules.some(r => r.shortPath === newShortPath)) return r409('新短路径已被占用');
data.rules[idx] = { shortPath: newShortPath, targetUrl, note: note || '' };
await saveRedirectData(env, data);
return r200({ success: true });
}
async function handleDeleteRule(req, env) {
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r403('管理功能未启用');
if (!(await requireAdmin(req, env))) return r403('未授权或令牌失效');
if (req.method !== 'DELETE' && req.method !== 'POST') return r405();
const { shortPath } = await req.json();
if (!shortPath) return r400('缺少必填字段');
const data = await loadRedirectData(env);
const filtered = data.rules.filter(r => r.shortPath !== shortPath);
if (filtered.length === data.rules.length) return r404('规则不存在');
data.rules = filtered;
await saveRedirectData(env, data);
return r200({ success: true });
}
async function handleUpdateRedirectSite(req, env) {
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r403('管理功能未启用');
if (!(await requireAdmin(req, env))) return r403('未授权或令牌失效');
if (req.method !== 'POST') return r405();
const { ...fields } = await req.json();
const data = await loadRedirectData(env);
const cfg = data.siteConfig;
if (fields.siteName) cfg.siteName = fields.siteName.trim();
if (fields.favicon) cfg.favicon = fields.favicon.trim();
if (fields.navTitle) cfg.navTitle = fields.navTitle.trim();
if (fields.titleLarge) cfg.titleLarge = fields.titleLarge.trim();
if (fields.titleSmall) cfg.titleSmall = fields.titleSmall.trim();
if (fields.notFoundUrl) cfg.notFoundUrl = fields.notFoundUrl.trim();
if (fields.copyrightText) cfg.copyrightText = fields.copyrightText.trim();
data.siteConfig = cfg;
await saveRedirectData(env, data);
return r200({ success: true, config: cfg });
}
async function handleUpdatePassword(req, env) {
const adminAuth = await getAdminAuth(env);
if (!adminAuth.hasAdmin) return r403('管理功能未启用');
if (!(await requireAdmin(req, env))) return r403('未授权或令牌失效');
if (req.method !== 'POST') return r405();
const { pass, newPassword } = await req.json();
if (!pass || !newPassword || !newPassword.trim()) return r400('缺少必填字段');
if (!(await verifyAdminPassword(pass, adminAuth))) return r403('旧密码错误');
let config = {};
try {
const raw = await env.KV_PAN.get('sys_config', { type: 'json' });
if (raw && typeof raw === 'object') config = raw;
} catch (e) {}
config.admin = newPassword.trim();
await env.KV_PAN.put('sys_config', JSON.stringify(config));
return r200({ success: true, message: '密码修改成功,请使用新密码重新登录。' });
}
function r200(data) { return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }); }
function r400(msg) { return new Response(JSON.stringify({ error: msg }), { status: 400, headers: { 'Content-Type': 'application/json' } }); }
function r403(msg) { return new Response(JSON.stringify({ error: msg }), { status: 403, headers: { 'Content-Type': 'application/json' } }); }
function r404(msg) { return new Response(JSON.stringify({ error: msg }), { status: 404, headers: { 'Content-Type': 'application/json' } }); }
function r405() { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); }
function r409(msg) { return new Response(JSON.stringify({ error: msg }), { status: 409, headers: { 'Content-Type': 'application/json' } }); }
function createRulesPage(rules, redirectCount, hasAdmin, siteConfig, host) {
const sortedRules = [...rules].sort((a, b) => {
if (a.shortPath === '/') return -1;
if (b.shortPath === '/') return 1;
return 0;
});
const rulesJSON = JSON.stringify(sortedRules).replace(/'/g, "\\'");
const {
siteName, favicon, navTitle, titleLarge, titleSmall, notFoundUrl, copyrightText
} = siteConfig;
const SPRITE_URL = 'https://www.langaj.cn/src/icon.svg';
const html = `
${escapeHtml(siteName)}
${escapeHtml(titleLarge)}
${escapeHtml(titleSmall)}
已服务重定向次数: ${redirectCount} 次。
${hasAdmin ? `
` : ''}
${sortedRules.map(rule => `
https://${host}${escapeHtml(rule.shortPath)}
跳转
备注:${escapeHtml(rule.note || '无')}
${hasAdmin ? `
` : ''}
`).join('')}
${hasAdmin ? `
确认删除
确定要删除该规则吗?删除后无法恢复。
` : ''}
`;
return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
function createNotFoundResponse(request, path, notFoundUrl) {
const host = new URL(request.url).host;
const errorUrl = `https://${host}${path}`;
const target = notFoundUrl || 'https://www.langaj.cn/404.html';
const separator = target.includes('?') ? '&' : '?';
return Response.redirect(`${target}${separator}s=${encodeURIComponent(errorUrl)}`, 302);
}
function escapeHtml(text) {
if (!text) return '';
return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}