Fix: Resolve account login issues in Node.js hosted version (#820)

# PR Checklist
-  Have you checked if it works normally in all models? *Ignore this if
it doesn't use models.*
   - Not applicable
-  Have you checked if it works normally in all web, local, and node
hosted versions? If it doesn't, have you blocked it in those versions?
   - Done
-  Have you added type definitions?
   - Not applicable

# Description

In the previous commit, a new proxy endpoint was added to the backend to
resolve CORS errors that occurred when the frontend directly fetched
data from Risu Realm.

However, 403 errors were occurring because the POST requests sent from
the loadRisuAccountData() function in the frontend were not being
properly processed.
Additionally, the backend was inefficiently handling complex body
processing.

The updated backend code now provides a more efficient and stable
solution by directly piping the original request.

Furthermore, an issue where UserSettings.svelte couldn't handle tokens
received through the backend proxy after successfully logging into a
Risu account has also been fixed.
This commit is contained in:
kwaroran
2025-04-28 15:41:26 +09:00
committed by GitHub
5 changed files with 65 additions and 100 deletions

View File

@@ -69,7 +69,6 @@
"mnemonist": "^0.40.3", "mnemonist": "^0.40.3",
"mobile-drag-drop": "3.0.0-rc.0", "mobile-drag-drop": "3.0.0-rc.0",
"msgpackr": "1.10.1", "msgpackr": "1.10.1",
"node-fetch": "2",
"node-html-parser": "^6.1.12", "node-html-parser": "^6.1.12",
"ollama": "^0.5.0", "ollama": "^0.5.0",
"pdfjs-dist": "^4.0.379", "pdfjs-dist": "^4.0.379",

18
pnpm-lock.yaml generated
View File

@@ -158,15 +158,15 @@ importers:
ml-distance: ml-distance:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
mnemonist:
specifier: ^0.40.3
version: 0.40.3
mobile-drag-drop: mobile-drag-drop:
specifier: 3.0.0-rc.0 specifier: 3.0.0-rc.0
version: 3.0.0-rc.0 version: 3.0.0-rc.0
msgpackr: msgpackr:
specifier: 1.10.1 specifier: 1.10.1
version: 1.10.1 version: 1.10.1
node-fetch:
specifier: '2'
version: 2.7.0
node-html-parser: node-html-parser:
specifier: ^6.1.12 specifier: ^6.1.12
version: 6.1.12 version: 6.1.12
@@ -2756,6 +2756,9 @@ packages:
ml-tree-similarity@1.0.0: ml-tree-similarity@1.0.0:
resolution: {integrity: sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg==} resolution: {integrity: sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg==}
mnemonist@0.40.3:
resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==}
mobile-drag-drop@3.0.0-rc.0: mobile-drag-drop@3.0.0-rc.0:
resolution: {integrity: sha512-f8wIDTbBYLBW/+5sei1cqUE+StyDpf/LP+FRZELlVX6tmOOmELk84r3wh1z3woxCB9G5octhF06K5COvFjGgqg==} resolution: {integrity: sha512-f8wIDTbBYLBW/+5sei1cqUE+StyDpf/LP+FRZELlVX6tmOOmELk84r3wh1z3woxCB9G5octhF06K5COvFjGgqg==}
@@ -2900,6 +2903,9 @@ packages:
object-inspect@1.13.1: object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
obliterator@2.0.5:
resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==}
ollama@0.5.0: ollama@0.5.0:
resolution: {integrity: sha512-CRtRzsho210EGdK52GrUMohA2pU+7NbgEaBG3DcYeRmvQthDO7E2LHOkLlUUeaYUlNmEd8icbjC02ug9meSYnw==} resolution: {integrity: sha512-CRtRzsho210EGdK52GrUMohA2pU+7NbgEaBG3DcYeRmvQthDO7E2LHOkLlUUeaYUlNmEd8icbjC02ug9meSYnw==}
@@ -6505,6 +6511,10 @@ snapshots:
binary-search: 1.3.6 binary-search: 1.3.6
num-sort: 2.1.0 num-sort: 2.1.0
mnemonist@0.40.3:
dependencies:
obliterator: 2.0.5
mobile-drag-drop@3.0.0-rc.0: {} mobile-drag-drop@3.0.0-rc.0: {}
modify-values@1.0.1: {} modify-values@1.0.1: {}
@@ -6665,6 +6675,8 @@ snapshots:
object-inspect@1.13.1: {} object-inspect@1.13.1: {}
obliterator@2.0.5: {}
ollama@0.5.0: ollama@0.5.0:
dependencies: dependencies:
whatwg-fetch: 3.6.20 whatwg-fetch: 3.6.20

View File

@@ -11,8 +11,7 @@ app.use(express.raw({ type: 'application/octet-stream', limit: '50mb' }));
const {pipeline} = require('stream/promises') const {pipeline} = require('stream/promises')
const https = require('https'); const https = require('https');
const sslPath = path.join(process.cwd(), 'server/node/ssl/certificate'); const sslPath = path.join(process.cwd(), 'server/node/ssl/certificate');
const EXTERNAL_HUB_URL = 'https://sv.risuai.xyz'; const hubURL = 'https://sv.risuai.xyz';
const fetch = require('node-fetch');
let password = '' let password = ''
@@ -31,12 +30,17 @@ function isHex(str) {
} }
app.get('/', async (req, res, next) => { app.get('/', async (req, res, next) => {
console.log("[Server] Connected")
const clientIP = req.headers['x-forwarded-for'] || req.ip || req.socket.remoteAddress || 'Unknown IP';
const timestamp = new Date().toISOString();
console.log(`[Server] ${timestamp} | Connection from: ${clientIP}`);
try { try {
const mainIndex = await fs.readFile(path.join(process.cwd(), 'dist', 'index.html')) const mainIndex = await fs.readFile(path.join(process.cwd(), 'dist', 'index.html'))
const root = htmlparser.parse(mainIndex) const root = htmlparser.parse(mainIndex)
const head = root.querySelector('head') const head = root.querySelector('head')
head.innerHTML = `<script>globalThis.__NODE__ = true</script>` + head.innerHTML head.innerHTML = `<script>globalThis.__NODE__ = true</script>` + head.innerHTML
res.send(root.toString()) res.send(root.toString())
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@@ -139,116 +143,69 @@ const reverseProxyFunc_get = async (req, res, next) => {
} }
} }
// Risu Realm Proxy async function hubProxyFunc(req, res) {
async function hubProxyHandler(req, res, next) {
try { try {
// Extract request path and query parameters
const pathAndQuery = req.originalUrl.replace(/^\/hub-proxy/, ''); const pathAndQuery = req.originalUrl.replace(/^\/hub-proxy/, '');
const externalURL = EXTERNAL_HUB_URL + pathAndQuery; const externalURL = hubURL + pathAndQuery;
console.log(`[Hub Proxy] Forwarding ${req.method} request to: ${externalURL}`);
// Prepare headers to send to the realm server (including Accept-Encoding modification)
const headersToSend = { ...req.headers }; const headersToSend = { ...req.headers };
delete headersToSend['host']; delete headersToSend.host;
delete headersToSend['connection']; delete headersToSend.connection;
headersToSend['accept-encoding'] = 'gzip, deflate'; // Exclude zstd, etc.
if (!headersToSend['x-forwarded-for']) {
headersToSend['x-forwarded-for'] = req.ip;
}
// Execute the fetch request to the realm server
const response = await fetch(externalURL, { const response = await fetch(externalURL, {
method: req.method, method: req.method,
headers: headersToSend, headers: headersToSend,
body: (req.method !== 'GET' && req.method !== 'HEAD') ? req.body : undefined, body: req.method !== 'GET' && req.method !== 'HEAD' ? req : undefined,
redirect: 'manual',
duplex: 'half'
}); });
console.log(`[Hub Proxy] Received status ${response.status} from external server`); for (const [key, value] of response.headers.entries()) {
res.setHeader(key, value);
// Handle the realm server response
// Clean up response headers and extract Content-Type
const responseHeaders = {};
// Check the Content-Type of the realm server response (use default if missing)
let contentType = response.headers.get('content-type') || 'application/octet-stream';
response.headers.forEach((value, key) => {
const lowerKey = key.toLowerCase();
// List of headers not to be forwarded to the client
const excludedHeaders = [
'transfer-encoding', 'connection', 'content-encoding',
'access-control-allow-origin', 'access-control-allow-methods',
'access-control-allow-headers', 'content-security-policy',
'content-security-policy-report-only', 'clear-site-data',
'strict-transport-security', 'expect-ct',
'cf-ray', 'cf-cache-status', 'report-to', 'nel', 'server', 'server-timing', 'alt-svc'
];
if (!excludedHeaders.includes(lowerKey)) {
responseHeaders[key] = value;
} }
}); res.status(response.status);
// Set the status code and cleaned headers for the client if (response.status >= 300 && response.status < 400) {
res.status(response.status).set(responseHeaders); // Redirect handling (due to /redirect/docs/lua)
const redirectUrl = response.headers.get('location');
if (redirectUrl) {
// Determine body processing method based on Content-Type if (redirectUrl.startsWith('http')) {
try {
if (contentType.startsWith('application/json')) {
// JSON response: read as text and send
const bodyText = await response.text();
console.log(`[Hub Proxy] Processing JSON response (size: ${bodyText.length})`);
res.setHeader('Content-Type', contentType); // Set the final Content-Type
res.send(bodyText);
} else if (contentType.startsWith('image/')) { if (redirectUrl.startsWith(hubURL)) {
// Image response: read as buffer and send const newPath = redirectUrl.replace(hubURL, '/hub-proxy');
const bodyBuffer = await response.buffer(); // Assuming 'fetch' response object has a .buffer() method or similar res.setHeader('location', newPath);
console.log(`[Hub Proxy] Processing Image response (type: ${contentType}, size: ${bodyBuffer.length} bytes)`);
res.setHeader('Content-Type', contentType); // Set the final Content-Type
res.send(bodyBuffer);
} else {
// Other responses (HTML, other text, unknown binary, etc.): read as buffer and send safely
const bodyBuffer = await response.buffer(); // Assuming 'fetch' response object has a .buffer() method or similar
console.log(`[Hub Proxy] Processing Other response as buffer (type: ${contentType}, size: ${bodyBuffer.length} bytes)`);
// Use original Content-Type if available, otherwise use octet-stream (already handled by default assignment)
res.setHeader('Content-Type', contentType);
res.send(bodyBuffer);
} }
} catch (bodyError) {
// If an error occurs while reading/processing the response body } else if (redirectUrl.startsWith('/')) {
console.error("[Hub Proxy] Error reading/processing response body:", bodyError);
if (!res.headersSent) { res.setHeader('location', `/hub-proxy${redirectUrl}`);
res.status(500).send({ error: 'Failed to process response body from hub server.' });
} else {
console.error("[Hub Proxy] Headers already sent, cannot send body error to client.");
res.end();
} }
return; // End the handler
} }
return res.end();
}
await pipeline(response.body, res);
} catch (error) { } catch (error) {
// Fetch request itself failed or other exceptions console.error("[Hub Proxy] Error:", error);
console.error("[Hub Proxy] Request failed:", error);
if (!res.headersSent) { if (!res.headersSent) {
res.status(502).send({ error: 'Proxy failed to connect to or get response from the hub server.' }); res.status(502).send({ error: 'Proxy request failed: ' + error.message });
} else { } else {
console.error("[Hub Proxy] Headers already sent, cannot send connection error to client.");
res.end(); res.end();
} }
} }
} }
app.get('/hub-proxy/*', hubProxyHandler);
app.post('/hub-proxy/*', hubProxyHandler);
app.put('/hub-proxy/*', hubProxyHandler);
app.get('/proxy', reverseProxyFunc_get); app.get('/proxy', reverseProxyFunc_get);
app.get('/proxy2', reverseProxyFunc_get); app.get('/proxy2', reverseProxyFunc_get);
app.get('/hub-proxy/*', hubProxyFunc);
app.post('/proxy', reverseProxyFunc); app.post('/proxy', reverseProxyFunc);
app.post('/proxy2', reverseProxyFunc); app.post('/proxy2', reverseProxyFunc);
app.post('/hub-proxy/*', hubProxyFunc);
app.get('/api/password', async(req, res)=> { app.get('/api/password', async(req, res)=> {
if(password === ''){ if(password === ''){
@@ -408,9 +365,6 @@ async function getHttpsOptions() {
const keyPath = path.join(sslPath, 'server.key'); const keyPath = path.join(sslPath, 'server.key');
const certPath = path.join(sslPath, 'server.crt'); const certPath = path.join(sslPath, 'server.crt');
console.log(keyPath)
console.log(certPath)
try { try {
await fs.access(keyPath); await fs.access(keyPath);

View File

@@ -72,7 +72,7 @@
</script> </script>
<svelte:window onmessage={async (e) => { <svelte:window onmessage={async (e) => {
if(e.origin.startsWith("https://sv.risuai.xyz") || e.origin.startsWith("http://127.0.0.1")){ if(e.origin.startsWith("https://sv.risuai.xyz") || e.origin.startsWith("http://127.0.0.1") || e.origin === window.location.origin){
if(e.data.msg.data.vaild && $alertStore.type === 'login'){ if(e.data.msg.data.vaild && $alertStore.type === 'login'){
$alertStore = { $alertStore = {
type: 'none', type: 'none',

View File

@@ -19,7 +19,7 @@
</script> </script>
<svelte:window onmessage={async (e) => { <svelte:window onmessage={async (e) => {
if(e.origin.startsWith("https://sv.risuai.xyz") || e.origin.startsWith("http://127.0.0.1")){ if(e.origin.startsWith("https://sv.risuai.xyz") || e.origin.startsWith("http://127.0.0.1") || e.origin === window.location.origin){
if(e.data.msg.type === 'drive'){ if(e.data.msg.type === 'drive'){
await loadRisuAccountData() await loadRisuAccountData()
DBState.db.account.data.refresh_token = e.data.msg.data.refresh_token DBState.db.account.data.refresh_token = e.data.msg.data.refresh_token