diff --git a/XMOJ.user.js b/XMOJ.user.js index d8c8c077..b68a45a0 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -6,6 +6,8 @@ // @namespace https://github/langningchen // @match *://*.xmoj.tech/* // @match *://116.62.212.172/* +// @match *://xmoj-bbs.me/messages.html +// @match *://www.xmoj-bbs.me/messages.html // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/mode/clike/clike.min.js @@ -25,6 +27,7 @@ // @supportURL https://support.xmoj-bbs.me/form/8050213e-c806-4680-b414-0d1c48263677 // @connect api.xmoj-bbs.tech // @connect api.xmoj-bbs.me +// @connect www.xmoj.tech // @connect challenges.cloudflare.com // @connect cppinsights.io // @connect cdnjs.cloudflare.com @@ -437,9 +440,7 @@ let UtilityEnabled = (Name) => { return localStorage.getItem("UserScript-Setting-" + Name) == "true"; } catch (e) { console.error(e); - if (UtilityEnabled("DebugMode")) { - SmartAlert("XMOJ-Script internal error!\n\n" + e + "\n\n" + "If you see this message, please report it to the developer.\nDon't forget to include console logs and a way to reproduce the error!\n\nDon't want to see this message? Disable DebugMode."); - } + return false; // Cannot call UtilityEnabled recursively here } }; let storeCredential = async (username, password) => { @@ -858,6 +859,83 @@ GM_registerMenuCommand("重置数据", () => { } }); +// === Short Messages WebUI helper — runs ONLY on xmoj-bbs.me/messages.html === +// On that page the rest of the userscript must not execute (no xmoj.tech DOM present). +if (/(?:^|\.)xmoj-bbs\.me$/.test(location.hostname) && location.pathname === '/messages.html') { + // Try to auto-fill the stored session from the user's active xmoj.tech cookie. + // Runs only when localStorage has no saved credentials yet. + (function () { + var hasStoredSession = !!(localStorage.getItem('xmoj-msg-username') + && localStorage.getItem('xmoj-msg-phpsessid')); + if (hasStoredSession) return; + + // Check both hostname variants: www.xmoj.tech and the raw IP (116.62.212.172) + Promise.all([ + GM.cookie.list({ name: 'PHPSESSID', domain: 'www.xmoj.tech' }).catch(function () { return []; }), + GM.cookie.list({ name: 'PHPSESSID', domain: '116.62.212.172' }).catch(function () { return []; }) + ]).then(function (results) { + var cookies = (results[0] || []).concat(results[1] || []); + { + var sessionCookie = cookies.find(function (c) { + return c.name === 'PHPSESSID'; + }); + if (!sessionCookie || !sessionCookie.value) { + // Not logged in — prompt the user after the page has rendered + setTimeout(function () { + var uEl = document.getElementById('manual-username'); + var pEl = document.getElementById('manual-phpsessid'); + if (!(uEl && uEl.value.trim()) && !(pEl && pEl.value.trim())) { + window.dispatchEvent(new CustomEvent('xmoj-show-toast', { + detail: { message: '请先登录 xmoj.tech,再使用短消息 WebUI' } + })); + } + }, 800); + return; + } + var phpsessid = sessionCookie.value; + // Fetch xmoj.tech home page to resolve the username from #profile + GM_xmlhttpRequest({ + method: 'GET', + url: 'https://www.xmoj.tech/', + onload: function (resp) { + var parser = new DOMParser(); + var doc = parser.parseFromString(resp.responseText, 'text/html'); + var profileEl = doc.querySelector('#profile'); + var username = profileEl + ? profileEl.innerText.replace(/[^a-zA-Z0-9]/g, '') : ''; + if (username) { + window.dispatchEvent(new CustomEvent('xmoj-autofill-session', { + detail: { username: username, phpsessid: phpsessid } + })); + } else { + // Got the cookie but couldn't parse username — pre-fill the field + var pEl = document.getElementById('manual-phpsessid'); + if (pEl) pEl.value = phpsessid; + window.dispatchEvent(new CustomEvent('xmoj-show-toast', { + detail: { message: '已填入会话,请输入用户名完成登录' } + })); + } + }, + onerror: function () { + var pEl = document.getElementById('manual-phpsessid'); + if (pEl) pEl.value = phpsessid; + window.dispatchEvent(new CustomEvent('xmoj-show-toast', { + detail: { message: '已填入 PHPSESSID,请手动输入用户名' } + })); + } + }); + } + }).catch(function () { + setTimeout(function () { + window.dispatchEvent(new CustomEvent('xmoj-show-toast', { + detail: { message: '请先登录 xmoj.tech,再使用短消息 WebUI' } + })); + }, 800); + }); + })(); + return; // Do not execute the rest of the userscript on xmoj-bbs.me +} + //otherwise CurrentUsername might be undefined if (UtilityEnabled("AutoLogin") && document.querySelector("body > a:nth-child(1)") != null && document.querySelector("body > a:nth-child(1)").innerText == "请登录后继续操作") { localStorage.setItem("UserScript-LastPage", location.pathname + location.search); @@ -865,9 +943,13 @@ if (UtilityEnabled("AutoLogin") && document.querySelector("body > a:nth-child(1) } let SearchParams = new URLSearchParams(location.search); -let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me") +let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-bbs.me/" : "https://www.xmoj-bbs.me"); if (document.querySelector("#profile") === null) { - location.href = "https://www.xmoj.tech/loginpage.php"; + // Do not redirect when running on xmoj-bbs.me/messages.html (no xmoj.tech DOM). + if (!/(?:^|\.)xmoj-bbs\.me$/.test(location.hostname)) { + location.href = "https://www.xmoj.tech/loginpage.php"; + } + return; } let CurrentUsername = document.querySelector("#profile").innerText; CurrentUsername = CurrentUsername.replaceAll(/[^a-zA-Z0-9]/g, ""); @@ -4801,7 +4883,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - Content.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + Content.value = Before + `![](https://api.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; Content.dispatchEvent(new Event("input")); } else { Content.value = Before + `![上传失败!` + ResponseData.Message + `]()` + After; @@ -5057,7 +5139,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.value = Before + `![](https://api.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; ContentElement.dispatchEvent(new Event("input")); } else { ContentElement.value = Before + `![上传失败!]()` + After; @@ -5065,8 +5147,7 @@ int main() } }); }; - } - } + } } } }); SubmitElement.addEventListener("click", async () => { @@ -5230,7 +5311,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - ContentElement.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentElement.value = Before + `![](https://api.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; ContentElement.dispatchEvent(new Event("input")); } else { ContentElement.value = Before + `![上传失败!]()` + After; @@ -5488,7 +5569,7 @@ int main() "Image": Reader.result }, (ResponseData) => { if (ResponseData.Success) { - ContentEditor.value = Before + `![](https://assets.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; + ContentEditor.value = Before + `![](https://api.xmoj-bbs.me/GetImage?ImageID=${ResponseData.Data.ImageID})` + After; ContentEditor.dispatchEvent(new Event("input")); } else { ContentEditor.value = Before + `![上传失败!]()` + After; diff --git a/functions/api-proxy/[[path]].js b/functions/api-proxy/[[path]].js new file mode 100644 index 00000000..ca4c64b1 --- /dev/null +++ b/functions/api-proxy/[[path]].js @@ -0,0 +1,91 @@ +/** + * Cloudflare Pages Function — CORS proxy for api.xmoj-bbs.me + * + * All fetch() calls in messages.html are directed here (same-origin, so no + * CORS preflight is ever blocked). This function forwards the POST body + * server-side to https://api.xmoj-bbs.me/, adds CORS response headers, and + * streams the reply back to the client. + * + * Security: + * • Only POST and OPTIONS (CORS preflight) methods are handled. + * • The constructed target URL is verified to still start with API_TARGET + * before the upstream request is made, preventing SSRF via absolute-URL + * injection in the path parameter. + * • Only a fixed allow-list of request headers is forwarded upstream. + */ + +const API_TARGET = 'https://api.xmoj-bbs.me/'; + +// Headers from the client that we forward to the upstream API. +const FORWARD_REQUEST_HEADERS = [ + 'content-type', + 'cache-control', + 'xmoj-userid', + 'xmoj-script-version', +]; + +const CORS_HEADERS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, XMOJ-UserID, XMOJ-Script-Version', + 'Access-Control-Max-Age': '86400', +}; + +/** Handle CORS preflight. */ +export async function onRequestOptions() { + return new Response(null, { status: 204, headers: CORS_HEADERS }); +} + +/** Proxy POST requests to api.xmoj-bbs.me. */ +export async function onRequestPost({ request, params }) { + // Build the path from the catch-all route parameter. + const pathSegments = Array.isArray(params.path) + ? params.path + : (params.path ? [params.path] : []); + const path = pathSegments.join('/'); + + // Resolve the target URL. new URL() normalises path traversal + // (e.g. "../../" stays within the same origin), but an absolute-URL + // payload would override the base — the guard below catches that. + let targetUrl; + try { + targetUrl = new URL(path, API_TARGET).href; + } catch (_) { + return new Response('Bad Request', { status: 400, headers: CORS_HEADERS }); + } + + // SSRF guard: the resolved URL must still point at api.xmoj-bbs.me. + if (!targetUrl.startsWith(API_TARGET)) { + return new Response('Forbidden', { status: 403, headers: CORS_HEADERS }); + } + + // Forward only the allow-listed headers to avoid leaking cookies etc. + const forwardHeaders = new Headers(); + for (const name of FORWARD_REQUEST_HEADERS) { + const value = request.headers.get(name); + if (value) forwardHeaders.set(name, value); + } + + let upstream; + try { + upstream = await fetch(targetUrl, { + method: 'POST', + headers: forwardHeaders, + body: request.body, + }); + } catch (err) { + return new Response( + JSON.stringify({ Success: false, Message: 'Proxy upstream error: ' + err.message }), + { status: 502, headers: { 'Content-Type': 'application/json', ...CORS_HEADERS } } + ); + } + + const responseHeaders = new Headers(CORS_HEADERS); + const ct = upstream.headers.get('content-type'); + if (ct) responseHeaders.set('Content-Type', ct); + + return new Response(upstream.body, { + status: upstream.status, + headers: responseHeaders, + }); +} diff --git a/index.html b/index.html index 268a37b2..285adc73 100644 --- a/index.html +++ b/index.html @@ -47,6 +47,12 @@ + @@ -129,7 +135,9 @@

介绍