Files
risuai/server/node/server.cjs
shirosaki-hana 1b62dc95ed Add duplex
2025-04-18 13:18:37 +09:00

415 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const app = express();
const path = require('path');
const htmlparser = require('node-html-parser');
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
const fs = require('fs/promises')
const crypto = require('crypto')
app.use(express.static(path.join(process.cwd(), 'dist'), {index: false}));
app.use(express.json({ limit: '50mb' }));
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 hubURL = 'https://sv.risuai.xyz';
let password = ''
const savePath = path.join(process.cwd(), "save")
if(!existsSync(savePath)){
mkdirSync(savePath)
}
const passwordPath = path.join(process.cwd(), 'save', '__password')
if(existsSync(passwordPath)){
password = readFileSync(passwordPath, 'utf-8')
}
const hexRegex = /^[0-9a-fA-F]+$/;
function isHex(str) {
return hexRegex.test(str.toUpperCase().trim()) || str === '__password';
}
app.get('/', async (req, res, next) => {
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 {
const mainIndex = await fs.readFile(path.join(process.cwd(), 'dist', 'index.html'))
const root = htmlparser.parse(mainIndex)
const head = root.querySelector('head')
head.innerHTML = `<script>globalThis.__NODE__ = true</script>` + head.innerHTML
res.send(root.toString())
} catch (error) {
console.log(error)
next(error)
}
})
const reverseProxyFunc = async (req, res, next) => {
const urlParam = req.headers['risu-url'] ? decodeURIComponent(req.headers['risu-url']) : req.query.url;
if (!urlParam) {
res.status(400).send({
error:'URL has no param'
});
return;
}
const header = req.headers['risu-header'] ? JSON.parse(decodeURIComponent(req.headers['risu-header'])) : req.headers;
if(!header['x-forwarded-for']){
header['x-forwarded-for'] = req.ip
}
let originalResponse;
try {
// make request to original server
originalResponse = await fetch(urlParam, {
method: req.method,
headers: header,
body: JSON.stringify(req.body)
});
// get response body as stream
const originalBody = originalResponse.body;
// get response headers
const head = new Headers(originalResponse.headers);
head.delete('content-security-policy');
head.delete('content-security-policy-report-only');
head.delete('clear-site-data');
head.delete('Cache-Control');
head.delete('Content-Encoding');
const headObj = {};
for (let [k, v] of head) {
headObj[k] = v;
}
// send response headers to client
res.header(headObj);
// send response status to client
res.status(originalResponse.status);
// send response body to client
await pipeline(originalResponse.body, res);
}
catch (err) {
next(err);
return;
}
}
const reverseProxyFunc_get = async (req, res, next) => {
const urlParam = req.headers['risu-url'] ? decodeURIComponent(req.headers['risu-url']) : req.query.url;
if (!urlParam) {
res.status(400).send({
error:'URL has no param'
});
return;
}
const header = req.headers['risu-header'] ? JSON.parse(decodeURIComponent(req.headers['risu-header'])) : req.headers;
if(!header['x-forwarded-for']){
header['x-forwarded-for'] = req.ip
}
let originalResponse;
try {
// make request to original server
originalResponse = await fetch(urlParam, {
method: 'GET',
headers: header
});
// get response body as stream
const originalBody = originalResponse.body;
// get response headers
const head = new Headers(originalResponse.headers);
head.delete('content-security-policy');
head.delete('content-security-policy-report-only');
head.delete('clear-site-data');
head.delete('Cache-Control');
head.delete('Content-Encoding');
const headObj = {};
for (let [k, v] of head) {
headObj[k] = v;
}
// send response headers to client
res.header(headObj);
// send response status to client
res.status(originalResponse.status);
// send response body to client
await pipeline(originalResponse.body, res);
}
catch (err) {
next(err);
return;
}
}
async function hubProxyFunc(req, res) {
try {
const pathAndQuery = req.originalUrl.replace(/^\/hub-proxy/, '');
const externalURL = hubURL + pathAndQuery;
const headersToSend = { ...req.headers };
delete headersToSend.host;
delete headersToSend.connection;
const response = await fetch(externalURL, {
method: req.method,
headers: headersToSend,
body: req.method !== 'GET' && req.method !== 'HEAD' ? req : undefined,
redirect: 'manual',
duplex: 'half'
});
for (const [key, value] of response.headers.entries()) {
res.setHeader(key, value);
}
res.status(response.status);
if (response.status >= 300 && response.status < 400) {
// Redirect handling (due to /redirect/docs/lua)
const redirectUrl = response.headers.get('location');
if (redirectUrl) {
if (redirectUrl.startsWith('http')) {
if (redirectUrl.startsWith(hubURL)) {
const newPath = redirectUrl.replace(hubURL, '/hub-proxy');
res.setHeader('location', newPath);
}
} else if (redirectUrl.startsWith('/')) {
res.setHeader('location', `/hub-proxy${redirectUrl}`);
}
}
return res.end();
}
await pipeline(response.body, res);
} catch (error) {
console.error("[Hub Proxy] Error:", error);
if (!res.headersSent) {
res.status(502).send({ error: 'Proxy request failed: ' + error.message });
} else {
res.end();
}
}
}
app.get('/proxy', reverseProxyFunc_get);
app.get('/proxy2', reverseProxyFunc_get);
app.get('/hub-proxy/*', hubProxyFunc);
app.post('/proxy', reverseProxyFunc);
app.post('/proxy2', reverseProxyFunc);
app.post('/hub-proxy/*', hubProxyFunc);
app.get('/api/password', async(req, res)=> {
if(password === ''){
res.send({status: 'unset'})
}
else if(req.headers['risu-auth'] === password){
res.send({status:'correct'})
}
else{
res.send({status:'incorrect'})
}
})
app.post('/api/crypto', async (req, res) => {
try {
const hash = crypto.createHash('sha256')
hash.update(Buffer.from(req.body.data, 'utf-8'))
res.send(hash.digest('hex'))
} catch (error) {
next(error)
}
})
app.post('/api/set_password', async (req, res) => {
if(password === ''){
password = req.body.password
writeFileSync(passwordPath, password, 'utf-8')
}
res.status(400).send("already set")
})
app.get('/api/read', async (req, res, next) => {
if(req.headers['risu-auth'].trim() !== password.trim()){
console.log('incorrect')
res.status(400).send({
error:'Password Incorrect'
});
return
}
const filePath = req.headers['file-path'];
if (!filePath) {
console.log('no path')
res.status(400).send({
error:'File path required'
});
return;
}
if(!isHex(filePath)){
res.status(400).send({
error:'Invaild Path'
});
return;
}
try {
if(!existsSync(path.join(savePath, filePath))){
res.send();
}
else{
res.setHeader('Content-Type','application/octet-stream');
res.sendFile(path.join(savePath, filePath));
}
} catch (error) {
next(error);
}
});
app.get('/api/remove', async (req, res, next) => {
if(req.headers['risu-auth'].trim() !== password.trim()){
console.log('incorrect')
res.status(400).send({
error:'Password Incorrect'
});
return
}
const filePath = req.headers['file-path'];
if (!filePath) {
res.status(400).send({
error:'File path required'
});
return;
}
if(!isHex(filePath)){
res.status(400).send({
error:'Invaild Path'
});
return;
}
try {
await fs.rm(path.join(savePath, filePath));
res.send({
success: true,
});
} catch (error) {
next(error);
}
});
app.get('/api/list', async (req, res, next) => {
if(req.headers['risu-auth'].trim() !== password.trim()){
console.log('incorrect')
res.status(400).send({
error:'Password Incorrect'
});
return
}
try {
const data = (await fs.readdir(path.join(savePath))).map((v) => {
return Buffer.from(v, 'hex').toString('utf-8')
})
res.send({
success: true,
content: data
});
} catch (error) {
next(error);
}
});
app.post('/api/write', async (req, res, next) => {
if(req.headers['risu-auth'].trim() !== password.trim()){
console.log('incorrect')
res.status(400).send({
error:'Password Incorrect'
});
return
}
const filePath = req.headers['file-path'];
const fileContent = req.body
if (!filePath || !fileContent) {
res.status(400).send({
error:'File path required'
});
return;
}
if(!isHex(filePath)){
res.status(400).send({
error:'Invaild Path'
});
return;
}
try {
await fs.writeFile(path.join(savePath, filePath), fileContent);
res.send({
success: true
});
} catch (error) {
next(error);
}
});
async function getHttpsOptions() {
const keyPath = path.join(sslPath, 'server.key');
const certPath = path.join(sslPath, 'server.crt');
try {
await fs.access(keyPath);
await fs.access(certPath);
const [key, cert] = await Promise.all([
fs.readFile(keyPath),
fs.readFile(certPath)
]);
return { key, cert };
} catch (error) {
console.error('[Server] SSL setup errors:', error.message);
console.log('[Server] Start the server with HTTP instead of HTTPS...');
return null;
}
}
async function startServer() {
try {
const port = process.env.PORT || 6001;
const httpsOptions = await getHttpsOptions();
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 () => {
await startServer();
})();