diff --git a/server/src/index.ts b/server/src/index.ts index 5740da4..2cb9e48 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,5 +1,6 @@ import express from 'express'; import path from 'path'; +import crypto from 'crypto'; import './db.js'; // Initialize database import subscriptionsRouter from './routes/subscriptions.js'; import nodesRouter from './routes/nodes.js'; @@ -8,16 +9,30 @@ import surgeRouter from './routes/surge.js'; import db from './db.js'; import { generateSurgeConfig } from './services/generator.js'; +// Ensure surge_token exists +function ensureSurgeToken(): string { + const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any; + if (row?.value) return row.value; + const token = crypto.randomUUID(); + db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('surge_token', ?)").run(token); + return token; +} +ensureSurgeToken(); + const app = express(); const PORT = parseInt(process.env.PORT || '3456', 10); app.use(express.json()); -// Surge endpoint (no auth, before everything) -app.get('/surge', (req, res) => { +// Surge endpoint (no auth, token-protected path) +app.get('/surge/:token', (req, res) => { + const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any; + if (!row?.value || req.params.token !== row.value) { + return res.status(404).send('Not Found'); + } const host = req.headers.host || 'localhost:3456'; const protocol = req.secure ? 'https' : 'http'; - const hostUrl = `${protocol}://${host}/surge`; + const hostUrl = `${protocol}://${host}/surge/${row.value}`; const config = generateSurgeConfig(hostUrl); res.set({ 'Content-Type': 'text/plain; charset=utf-8', @@ -89,12 +104,13 @@ app.get('/api/stats', (_req, res) => { const webDist = path.join(__dirname, '..', '..', 'web', 'dist'); app.use(express.static(webDist)); app.get('*', (req, res, next) => { - if (req.path.startsWith('/api') || req.path === '/surge') return next(); + if (req.path.startsWith('/api') || req.path.startsWith('/surge')) return next(); res.sendFile(path.join(webDist, 'index.html')); }); app.listen(PORT, () => { + const token = ensureSurgeToken(); console.log(`Sub Router running at http://127.0.0.1:${PORT}`); - console.log(`Surge subscription: http://127.0.0.1:${PORT}/surge`); + console.log(`Surge subscription: http://127.0.0.1:${PORT}/surge/${token}`); console.log(`Admin panel: http://127.0.0.1:${PORT}`); }); diff --git a/server/src/routes/surge.ts b/server/src/routes/surge.ts index 607cb13..019b6c8 100644 --- a/server/src/routes/surge.ts +++ b/server/src/routes/surge.ts @@ -1,28 +1,30 @@ import { Router } from 'express'; +import crypto from 'crypto'; +import db from '../db.js'; import { generateSurgeConfig } from '../services/generator.js'; const router = Router(); -// GET /surge - Surge client subscription endpoint (no auth required) -router.get('/', (req, res) => { - const host = req.headers.host || 'localhost:3456'; - const protocol = req.secure ? 'https' : 'http'; - const hostUrl = `${protocol}://${host}/surge`; +// GET /api/config/surge-token - get current surge token +router.get('/surge-token', (_req, res) => { + const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any; + res.json({ token: row?.value || null }); +}); - const config = generateSurgeConfig(hostUrl); - - res.set({ - 'Content-Type': 'text/plain; charset=utf-8', - 'Content-Disposition': 'attachment; filename=sub-router.conf', - }); - res.send(config); +// POST /api/config/surge-token - regenerate surge token +router.post('/surge-token', (_req, res) => { + const token = crypto.randomUUID(); + db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('surge_token', ?)").run(token); + res.json({ token }); }); // GET /api/config/preview - preview generated config router.get('/preview', (req, res) => { const host = req.headers.host || 'localhost:3456'; const protocol = req.secure ? 'https' : 'http'; - const hostUrl = `${protocol}://${host}/surge`; + const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any; + const token = row?.value || ''; + const hostUrl = `${protocol}://${host}/surge/${token}`; const config = generateSurgeConfig(hostUrl); res.json({ config }); diff --git a/web/src/api.ts b/web/src/api.ts index 65444bf..b3a1233 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -114,6 +114,8 @@ export const rules = { // Config export const config = { preview: () => request<{ config: string }>('/config/preview'), + getSurgeToken: () => request<{ token: string }>('/config/surge-token'), + regenerateSurgeToken: () => request<{ token: string }>('/config/surge-token', { method: 'POST' }), }; // Stats diff --git a/web/src/components/Output.tsx b/web/src/components/Output.tsx index b274d1e..a361b2d 100644 --- a/web/src/components/Output.tsx +++ b/web/src/components/Output.tsx @@ -5,8 +5,25 @@ export default function Output() { const [preview, setPreview] = useState(''); const [loading, setLoading] = useState(false); const [copied, setCopied] = useState(false); + const [surgeToken, setSurgeToken] = useState(''); - const surgeUrl = `${window.location.origin}/surge`; + const surgeUrl = surgeToken ? `${window.location.origin}/surge/${surgeToken}` : ''; + + const loadToken = async () => { + try { + const data = await configApi.getSurgeToken(); + setSurgeToken(data.token || ''); + } catch {} + }; + + const handleRegenerate = async () => { + if (!confirm('重新生成后,旧的订阅链接将失效,Surge 客户端需要更新订阅地址。确定继续?')) return; + try { + const data = await configApi.regenerateSurgeToken(); + setSurgeToken(data.token); + loadPreview(); + } catch {} + }; const loadPreview = async () => { setLoading(true); @@ -20,7 +37,10 @@ export default function Output() { } }; - useEffect(() => { loadPreview(); }, []); + useEffect(() => { + loadToken(); + loadPreview(); + }, []); const handleCopy = () => { navigator.clipboard.writeText(surgeUrl); @@ -58,12 +78,18 @@ export default function Output() { fontSize: 13, color: 'var(--accent)', userSelect: 'all', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', }}> - {surgeUrl} + {surgeUrl || '加载中...'} - +