Files
risuai/server/node/server.cjs

423 lines
11 KiB
JavaScript
Raw Permalink 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 { CosmosStore } = require("./db.cjs");
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 cosmosStore = new CosmosStore(
"http://127.0.0.1:8081",
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"db",
"container",
);
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 {
try {
const fileData = await cosmosStore.getData(filePath);
res.setHeader("Content-Type", "application/octet-stream");
res.send(fileData);
} catch (e) {
res.send();
}
if (!existsSync(path.join(savePath, filePath))) {
} else {
}
} 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 cosmosStore.removeData(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 cosmosStore.listData()).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 cosmosStore.createData(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();
})();