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 => `

${rule.shortPath === '/' ? '根路径' : escapeHtml(rule.shortPath)}

${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, '''); }