feat: uuid for sub link
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import crypto from 'crypto';
|
||||||
import './db.js'; // Initialize database
|
import './db.js'; // Initialize database
|
||||||
import subscriptionsRouter from './routes/subscriptions.js';
|
import subscriptionsRouter from './routes/subscriptions.js';
|
||||||
import nodesRouter from './routes/nodes.js';
|
import nodesRouter from './routes/nodes.js';
|
||||||
@@ -8,16 +9,30 @@ import surgeRouter from './routes/surge.js';
|
|||||||
import db from './db.js';
|
import db from './db.js';
|
||||||
import { generateSurgeConfig } from './services/generator.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 app = express();
|
||||||
const PORT = parseInt(process.env.PORT || '3456', 10);
|
const PORT = parseInt(process.env.PORT || '3456', 10);
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Surge endpoint (no auth, before everything)
|
// Surge endpoint (no auth, token-protected path)
|
||||||
app.get('/surge', (req, res) => {
|
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 host = req.headers.host || 'localhost:3456';
|
||||||
const protocol = req.secure ? 'https' : 'http';
|
const protocol = req.secure ? 'https' : 'http';
|
||||||
const hostUrl = `${protocol}://${host}/surge`;
|
const hostUrl = `${protocol}://${host}/surge/${row.value}`;
|
||||||
const config = generateSurgeConfig(hostUrl);
|
const config = generateSurgeConfig(hostUrl);
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
@@ -89,12 +104,13 @@ app.get('/api/stats', (_req, res) => {
|
|||||||
const webDist = path.join(__dirname, '..', '..', 'web', 'dist');
|
const webDist = path.join(__dirname, '..', '..', 'web', 'dist');
|
||||||
app.use(express.static(webDist));
|
app.use(express.static(webDist));
|
||||||
app.get('*', (req, res, next) => {
|
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'));
|
res.sendFile(path.join(webDist, 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
|
const token = ensureSurgeToken();
|
||||||
console.log(`Sub Router running at http://127.0.0.1:${PORT}`);
|
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}`);
|
console.log(`Admin panel: http://127.0.0.1:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import db from '../db.js';
|
||||||
import { generateSurgeConfig } from '../services/generator.js';
|
import { generateSurgeConfig } from '../services/generator.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// GET /surge - Surge client subscription endpoint (no auth required)
|
// GET /api/config/surge-token - get current surge token
|
||||||
router.get('/', (req, res) => {
|
router.get('/surge-token', (_req, res) => {
|
||||||
const host = req.headers.host || 'localhost:3456';
|
const row = db.prepare("SELECT value FROM config WHERE key = 'surge_token'").get() as any;
|
||||||
const protocol = req.secure ? 'https' : 'http';
|
res.json({ token: row?.value || null });
|
||||||
const hostUrl = `${protocol}://${host}/surge`;
|
});
|
||||||
|
|
||||||
const config = generateSurgeConfig(hostUrl);
|
// POST /api/config/surge-token - regenerate surge token
|
||||||
|
router.post('/surge-token', (_req, res) => {
|
||||||
res.set({
|
const token = crypto.randomUUID();
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('surge_token', ?)").run(token);
|
||||||
'Content-Disposition': 'attachment; filename=sub-router.conf',
|
res.json({ token });
|
||||||
});
|
|
||||||
res.send(config);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/config/preview - preview generated config
|
// GET /api/config/preview - preview generated config
|
||||||
router.get('/preview', (req, res) => {
|
router.get('/preview', (req, res) => {
|
||||||
const host = req.headers.host || 'localhost:3456';
|
const host = req.headers.host || 'localhost:3456';
|
||||||
const protocol = req.secure ? 'https' : 'http';
|
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);
|
const config = generateSurgeConfig(hostUrl);
|
||||||
res.json({ config });
|
res.json({ config });
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ export const rules = {
|
|||||||
// Config
|
// Config
|
||||||
export const config = {
|
export const config = {
|
||||||
preview: () => request<{ config: string }>('/config/preview'),
|
preview: () => request<{ config: string }>('/config/preview'),
|
||||||
|
getSurgeToken: () => request<{ token: string }>('/config/surge-token'),
|
||||||
|
regenerateSurgeToken: () => request<{ token: string }>('/config/surge-token', { method: 'POST' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
|
|||||||
@@ -5,8 +5,25 @@ export default function Output() {
|
|||||||
const [preview, setPreview] = useState('');
|
const [preview, setPreview] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [copied, setCopied] = 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 () => {
|
const loadPreview = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -20,7 +37,10 @@ export default function Output() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { loadPreview(); }, []);
|
useEffect(() => {
|
||||||
|
loadToken();
|
||||||
|
loadPreview();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(surgeUrl);
|
navigator.clipboard.writeText(surgeUrl);
|
||||||
@@ -58,12 +78,18 @@ export default function Output() {
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: 'var(--accent)',
|
color: 'var(--accent)',
|
||||||
userSelect: 'all',
|
userSelect: 'all',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
}}>
|
}}>
|
||||||
{surgeUrl}
|
{surgeUrl || '加载中...'}
|
||||||
</code>
|
</code>
|
||||||
<button className="small" onClick={handleCopy}>
|
<button className="small" onClick={handleCopy} disabled={!surgeUrl}>
|
||||||
{copied ? '已复制' : '复制'}
|
{copied ? '已复制' : '复制'}
|
||||||
</button>
|
</button>
|
||||||
|
<button className="small" onClick={handleRegenerate} disabled={!surgeUrl}>
|
||||||
|
重新生成
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user