Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 92 additions & 11 deletions XMOJ.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -858,16 +859,97 @@ 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);
location.href = "https://www.xmoj.tech/loginpage.php";
}

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, "");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -5057,16 +5139,15 @@ 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;
ContentElement.dispatchEvent(new Event("input"));
}
});
};
}
}
} }
}
});
SubmitElement.addEventListener("click", async () => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
91 changes: 91 additions & 0 deletions functions/api-proxy/[[path]].js
Original file line number Diff line number Diff line change
@@ -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,
});
}
10 changes: 9 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
<li class="nav-item">
<a class="nav-link" href="#About">关于</a>
</li>
<li class="nav-item">
<a class="nav-link" href="messages.html">
短消息 WebUI
<span class="badge bg-warning text-dark ms-1">Alpha</span>
</a>
</li>
</ul>
</div>
</div>
Expand Down Expand Up @@ -129,7 +135,9 @@ <h2>介绍</h2>
<ul class="list-group">
<li class="list-group-item"><b>比赛ACM排名与下载功能</b>:允许用户查看比赛的ACM排名,并提供下载选项,方便离线查阅。</li>
<li class="list-group-item"><b>讨论区</b>:我们自行搭建了一个讨论服务,你可以在里面发表你的声音。</li>
<li class="list-group-item"><b>短消息</b>:我们自行搭建了一个短消息服务,你可以在这里和你最好的伙伴交流。</li>
<li class="list-group-item"><b>短消息</b>:我们自行搭建了一个短消息服务,你可以在这里和你最好的伙伴交流。
<a href="messages.html" class="ms-2">短消息 WebUI <span class="badge bg-warning text-dark">Alpha</span></a>
— 无需安装脚本,支持 iOS/iPadOS 等移动设备直接访问。</li>
<li class="list-group-item"><b>查看更多标程</b>:展示更多的标准程序代码,帮助用户更好地理解题目要求和正确解法。</li>
<li class="list-group-item"><b>获取别人的测试点数据</b>:允许用户获取其他人的测试点数据,用于分析问题和优化代码。</li>
<li class="list-group-item"><b>自动刷新比赛列表与排名</b>:使比赛列表和排名页面自动定时刷新,获取最新信息。</li>
Expand Down
Loading
Loading