登录页去掉订阅口令:订阅接口认后台会话 + editor-api 反代
- Worker 侧 requireAuth 新增两条通道:① Authorization 管理员会话(CF 版后台 登录后订阅模块免口令);② X-Editor-Token 共享令牌(服务端到服务端)。 原 ?token=/Cookie 通道保留,RSS 阅读器与友圈页不受影响 - editor-api 新增订阅反代(/api/feeds|links|link-apply):国内机后台的 订阅源/友链由本机带共享令牌去 CF 取,前端不再持有任何口令;GET 幂等重试 抗跨境抖动 - 登录页只留账号+密码(CF 版与国内机版统一);国内机后台侧栏新增订阅中心 入口;RSS_TOKEN / LS_RSS 前端残留全部清理
This commit is contained in:
1 parent
a11631faa6
commit
18a4882577
4 files changed
+114
-51
No files matched your search
@@ -37,6 +37,10 @@ const cfg = {
|
||||
// 博客对外地址(如 https://usj.cc)。配了才返回绝对链接,列表「预览」按钮才能直接开新窗口。
|
||||
blogBase: (env.BLOG_BASE || '').replace(/\/+$/, ''),
|
||||
|
||||
// 订阅服务所在(Cloudflare 上的 rss-api)。国内机后台的订阅/友链请求由本服务
|
||||
// 反代过去,并带上共享令牌 —— 这样后台前端不必再持有「订阅口令」。
|
||||
rssBase: (env.RSS_API_BASE || 'https://api.200181.xyz').replace(/\/+$/, ''),
|
||||
|
||||
// --- 后台登录(浏览器直连本机时用,与 Cloudflare 会话是两套)---------------
|
||||
// 背景:后台静态页部署在国内机,nginx 的原生 basic auth 弹窗既丑又没法做品牌,
|
||||
// 所以改成「自建登录页 + HttpOnly Cookie 会话」。令牌通道(X-Editor-Token)
|
||||
@@ -212,6 +216,60 @@ function noteFailure(ip) {
|
||||
failures.set(ip, f);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 订阅反代
|
||||
|
||||
/** 需要转给订阅服务的路径前缀(后台的订阅源 / 友链模块用) */
|
||||
const RSS_PATH_PREFIXES = (env.RSS_PATHS || '/api/feeds,/api/links,/api/link-apply')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
/**
|
||||
* 把订阅/友链请求原样转发到订阅服务(Cloudflare 上的 rss-api),带上共享令牌。
|
||||
* 用途:国内机后台的订阅源 / 友链模块 —— 请求由本机出去,前端不需要知道
|
||||
* 订阅口令,也不必为了这两个模块去登录 Cloudflare 后台。
|
||||
*
|
||||
* 鉴权在调用处完成(会话或令牌),这里只管转发。
|
||||
*/
|
||||
async function rssProxy(req, res, url) {
|
||||
const target = cfg.rssBase + url.pathname + url.search;
|
||||
const method = (req.method || 'GET').toUpperCase();
|
||||
|
||||
const headers = { 'X-Editor-Token': cfg.token, Accept: 'application/json' };
|
||||
const ct = req.headers['content-type'];
|
||||
if (ct) headers['Content-Type'] = ct;
|
||||
|
||||
let body;
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
const buf = await readBody(req, 4 * 1024 * 1024);
|
||||
if (buf.length) body = buf;
|
||||
}
|
||||
|
||||
let r;
|
||||
// 跨境那一跳会偶发握手失败(实测直连 CF 有时 0.3s 就断)。GET 是幂等的,
|
||||
// 重试一次基本就过去了 —— 不能让用户看到「订阅源加载失败」。
|
||||
const tries = method === 'GET' || method === 'HEAD' ? 2 : 1;
|
||||
for (let i = 0; i < tries; i++) {
|
||||
if (i) await new Promise((s) => setTimeout(s, 400));
|
||||
try {
|
||||
r = await fetch(target, { method, headers, body });
|
||||
break;
|
||||
} catch (e) {
|
||||
if (i === tries - 1) {
|
||||
throw Object.assign(new Error('订阅服务连不上:' + e.message), { status: 502 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
res.writeHead(r.status, {
|
||||
'Content-Type': r.headers.get('content-type') || 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Content-Length': buf.length,
|
||||
});
|
||||
res.end(buf);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 路由
|
||||
|
||||
const routes = [];
|
||||
@@ -392,6 +450,13 @@ const server = http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 订阅 / 友链:反代到 Cloudflare 上的订阅服务,带上共享令牌。
|
||||
// 这样国内机后台也能管订阅,且前端不需要持有订阅口令。
|
||||
if (RSS_PATH_PREFIXES.some((p) => url.pathname === p || url.pathname.startsWith(p + '/'))) {
|
||||
await rssProxy(req, res, url);
|
||||
return;
|
||||
}
|
||||
|
||||
const hit = match(req.method, url.pathname);
|
||||
if (!hit) {
|
||||
json(res, 404, { error: 'Not Found: ' + req.method + ' ' + url.pathname });
|
||||
|
||||
Reference in new issue
Block a user