feat: add CORS proxy for subscription fetch

This commit is contained in:
2026-06-14 17:04:23 +08:00
parent 37e52cf186
commit 2002238a0b
2 changed files with 29 additions and 28 deletions
+14 -28
View File
@@ -431,15 +431,17 @@
if (!url) return toast('请输入订阅链接', 'error');
try {
toast('正在从浏览器获取订阅...', 'info');
toast('正在获取订阅...', 'info');
// Fetch subscription from browser side (avoids server network issues)
const resp = await fetch(url, {
headers: { 'User-Agent': 'ClashForAndroid/2.5.12' },
mode: 'cors',
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
let content = await resp.text();
// Use server-side proxy to fetch (bypasses CORS)
const fetchRes = await api('POST', '/api/proxy-fetch', { url });
if (fetchRes.error) throw new Error(fetchRes.error);
let content = fetchRes.content;
// Check if we got HTML instead of YAML
if (content.trim().startsWith('<!DOCTYPE') || content.trim().startsWith('<html')) {
throw new Error('订阅地址返回了网页而非配置,请检查链接是否正确');
}
// Try base64 decode
try {
@@ -447,13 +449,13 @@
if (decoded.includes('://') || decoded.includes('proxies')) content = decoded;
} catch {}
if (!content.includes('proxies') && !content.includes('port:')) {
throw new Error('无效的订阅内容');
if (!content.includes('proxies') && !content.includes('port:') && !content.includes('mixed-port:')) {
throw new Error('无效的订阅内容,请检查链接');
}
toast('订阅获取成功,正在推送到 mihomo...', 'info');
// Send content to mihomo via our server API
// Push content to mihomo
const applyRes = await api('POST', '/api/subscription/push', { content, url });
if (applyRes.error) throw new Error(applyRes.error);
@@ -463,23 +465,7 @@
loadSubscriptions();
loadDashboard();
} catch(e) {
// CORS blocked - fallback to server-side fetch
if (e.message.includes('Failed to fetch') || e.message.includes('NetworkError')) {
toast('浏览器无法直接获取(CORS),改用服务器获取...', 'info');
try {
const res = await api('POST', '/api/subscription/import', { url });
if (res.error) throw new Error(res.error);
const applyRes = await api('POST', '/api/subscription/apply', { url });
if (applyRes.error) throw new Error(applyRes.error);
toast('🎉 订阅导入成功!', 'success');
document.getElementById('sub-url').value = '';
loadSubscriptions();
} catch(e2) {
toast('导入失败: ' + e2.message, 'error');
}
} else {
toast('导入失败: ' + e.message, 'error');
}
toast('导入失败: ' + e.message, 'error');
}
}
+15
View File
@@ -355,6 +355,21 @@ async function handleApi(req, res, apiPath) {
}
}
// POST /api/proxy-fetch - CORS proxy: fetch URL on behalf of browser
if (apiPath === '/api/proxy-fetch' && method === 'POST') {
const body = JSON.parse(await readBody(req));
const { url } = body;
if (!url) return sendError(res, 'Missing url', 400);
try {
const result = await fetchUrl(url, { headers: { 'User-Agent': 'ClashForAndroid/2.5.12' } });
const content = result.body.toString('utf-8');
return sendJson(res, { ok: true, content, status: result.status });
} catch (e) {
return sendError(res, 'Fetch failed: ' + e.message);
}
}
// POST /api/subscription/push - Push subscription content directly to mihomo
if (apiPath === '/api/subscription/push' && method === 'POST') {
const body = JSON.parse(await readBody(req));