Fix: Resolve Realm CORS Violation for Node.js Hosted Version (#814)

# 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
## Problem
The Node.js hosted version of RisuAI encountered an issue where it
failed to fetch data from the Risu Realm server when accessed remotely.

RisuAI's frontend directly fetches data from the Realm server (e.g.,
sv.risuai.xyz). While the official web version did not exhibit CORS
errors (potentially due to same-origin deployment or specific
server-side CORS configurations), running the Node.js version on a
self-hosted server and accessing it remotely resulted in browser CORS
policy violations.

## Solution
The fix involves detecting when the frontend runs in the Node.js host
environment.

When this environment is detected, instead of requesting Realm data
directly from the external server (sv.risuai.xyz), the frontend now
directs the request to a new proxy endpoint (`/hub-proxy/*`) on its own
backend server.

The backend proxy then fetches the required data (including JSON and
images) from the actual Realm server, correctly handles content types
and compression, and relays the response back to the frontend.

This ensures that, from the browser's perspective, the frontend is
communicating with its same-origin backend, effectively bypassing
browser CORS restrictions and resolving the data fetching issue.
This commit is contained in:
kwaroran
2025-04-14 14:07:10 +09:00
committed by GitHub
4 changed files with 141 additions and 25 deletions

View File

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

3
pnpm-lock.yaml generated
View File

@@ -164,6 +164,9 @@ importers:
msgpackr:
specifier: 1.10.1
version: 1.10.1
node-fetch:
specifier: '2'
version: 2.7.0
node-html-parser:
specifier: ^6.1.12
version: 6.1.12

View File

@@ -11,6 +11,8 @@ app.use(express.raw({ type: 'application/octet-stream', limit: '50mb' }));
const {pipeline} = require('stream/promises')
const https = require('https');
const sslPath = path.join(process.cwd(), 'server/node/ssl/certificate');
const EXTERNAL_HUB_URL = 'https://sv.risuai.xyz';
const fetch = require('node-fetch');
let password = ''
@@ -29,7 +31,7 @@ function isHex(str) {
}
app.get('/', async (req, res, next) => {
console.log("connected")
console.log("[Server] Connected")
try {
const mainIndex = await fs.readFile(path.join(process.cwd(), 'dist', 'index.html'))
const root = htmlparser.parse(mainIndex)
@@ -137,12 +139,117 @@ const reverseProxyFunc_get = async (req, res, next) => {
}
}
// Risu Realm Proxy
async function hubProxyHandler(req, res, next) {
try {
// Extract request path and query parameters
const pathAndQuery = req.originalUrl.replace(/^\/hub-proxy/, '');
const externalURL = EXTERNAL_HUB_URL + 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 };
delete headersToSend['host'];
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, {
method: req.method,
headers: headersToSend,
body: (req.method !== 'GET' && req.method !== 'HEAD') ? req.body : undefined,
});
console.log(`[Hub Proxy] Received status ${response.status} from external server`);
// 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;
}
});
// Set the status code and cleaned headers for the client
res.status(response.status).set(responseHeaders);
// Determine body processing method based on Content-Type
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/')) {
// Image response: read as buffer and send
const bodyBuffer = await response.buffer(); // Assuming 'fetch' response object has a .buffer() method or similar
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
console.error("[Hub Proxy] Error reading/processing response body:", bodyError);
if (!res.headersSent) {
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
}
} catch (error) {
// Fetch request itself failed or other exceptions
console.error("[Hub Proxy] Request failed:", error);
if (!res.headersSent) {
res.status(502).send({ error: 'Proxy failed to connect to or get response from the hub server.' });
} else {
console.error("[Hub Proxy] Headers already sent, cannot send connection error to client.");
res.end();
}
}
}
app.get('/hub-proxy/*', hubProxyHandler);
app.post('/hub-proxy/*', hubProxyHandler);
app.put('/hub-proxy/*', hubProxyHandler);
app.get('/proxy', reverseProxyFunc_get);
app.get('/proxy2', reverseProxyFunc_get);
app.post('/proxy', reverseProxyFunc);
app.post('/proxy2', reverseProxyFunc);
app.get('/api/password', async(req, res)=> {
if(password === ''){
res.send({status: 'unset'})
@@ -317,36 +424,37 @@ async function getHttpsOptions() {
return { key, cert };
} catch (error) {
console.error('SSL setup errors:', error.message);
console.log('Start the server with HTTP instead of HTTPS...');
console.error('[Server] SSL setup errors:', error.message);
console.log('[Server] Start the server with HTTP instead of HTTPS...');
return null;
}
}
async function startServer() {
const port = process.env.PORT || 6001;
const httpsOptions = await getHttpsOptions();
try {
const port = process.env.PORT || 6001;
const httpsOptions = await getHttpsOptions();
if (httpsOptions) {
// HTTPS
https.createServer(httpsOptions, app).listen(port, () => {
console.log("HTTPS server is running.");
console.log("https://localhost:6001/");
});
} else {
// HTTP
app.listen(port, () => {
console.log("HTTP server is running.");
console.log("http://localhost:6001/");
});
if (httpsOptions) {
// HTTPS
https.createServer(httpsOptions, app).listen(port, () => {
console.log("[Server] HTTPS server is running.");
console.log(`[Server] https://localhost:${port}/`);
});
} else {
// HTTP
app.listen(port, () => {
console.log("[Server] HTTP server is running.");
console.log(`[Server] http://localhost:${port}/`);
});
}
} catch (error) {
console.error('[Server] Failed to start server :', error);
process.exit(1);
}
}
(async () => {
try {
await startServer();
} catch (error) {
console.error('Fail to start server :', error);
}
})();
await startServer();
})();

View File

@@ -18,7 +18,11 @@ import { exportModule, readModule, type RisuModule } from "./process/modules"
import { readFile } from "@tauri-apps/plugin-fs"
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
export const hubURL = "https://sv.risuai.xyz"
const EXTERNAL_HUB_URL = 'https://sv.risuai.xyz';
export const hubURL = typeof window !== 'undefined' && (window as any).__NODE__ === true
? '/hub-proxy'
: EXTERNAL_HUB_URL;
export async function importCharacter() {
try {