fuck it call it v0

This commit is contained in:
Rph :3 2025-06-06 21:32:21 +02:00
parent e51ccfa79c
commit 2c24f0878f
No known key found for this signature in database
7 changed files with 304 additions and 155 deletions

View File

@ -2,3 +2,4 @@ DEFAULT_USER_PASSWORD=
ROOT_DOMAIN=
DATA_ROOT=
PORT=
PROTO=

View File

@ -59,3 +59,34 @@ export function sessionValidate(session: string): string|null {
const row = sessionUserResponse.parse(rawrow);
return row.user;
}
const canCreateNewUsersResponse = z.object({
allowNewUserCreation: z.number()
});
export function canCreateNewUsers(username: string): boolean {
const rawrow = db.prepare("SELECT allowNewUserCreation FROM users WHERE username = ?").get(username);
if (!rawrow) {
return false;
}
const row = canCreateNewUsersResponse.parse(rawrow);
return row.allowNewUserCreation === 1;
}
export async function createNewUser(username: string, password: string, allowNewUserCreation: boolean) {
db.prepare(`INSERT INTO users(username, passwordHash, allowNewUserCreation) VALUES(?, ?, ?)
ON CONFLICT DO NOTHING`).run(username, await argon2.hash(password, {
timeCost: 5
}), allowNewUserCreation ? 1 : 0);
}
export async function updatePassword(username: string, password: string) {
db.prepare(`UPDATE users SET passwordHash = ? WHERE username = ?`).run(await argon2.hash(password, {
timeCost: 5
}), username);
}
export async function deleteSession(sid: string) {
db.prepare(`DELETE FROM sessions WHERE id = ?`).run(sid);
}

View File

@ -20,3 +20,8 @@ if (!process.env["PORT"] || !parseInt(process.env["PORT"]) || parseInt(process.e
throw new Error("PORT must be set and be an integer between 1 and 65535");
}
export const Port: number = parseInt(process.env["PORT"]);
if (!process.env["PROTO"]) {
throw new Error("PROTO must be set");
}
export const Protocol: string = process.env["PROTO"];

View File

@ -45,12 +45,12 @@ export function getGame(name: string): z.infer<typeof manifest> {
return manifestCache.get(name)!;
}
export function getLocalStorageDatabaseForUser(name: string, user: string): Database.Database {
export function getStorageDatabaseForUser(storageName: string, name: string, user: string): Database.Database {
if (!manifestCache.has(name)) throw new Error("no game (ps5 reference)");
mkdirSync(join(DataRoot, "storage", user, name), { recursive: true });
const db = new Database(join(DataRoot, "storage", user, name, "localStorage.db"));
const db = new Database(join(DataRoot, "storage", user, name, storageName + ".db"));
db.pragma("journal_mode = WAL");
db.exec("CREATE TABLE IF NOT EXISTS kv(key TEXT PRIMARY KEY, value TEXT)");

View File

@ -1,7 +1,7 @@
import express from "express";
import { sessionValidate } from "./auth.js";
import "./game.js";
import { getGame, getLocalStorageDatabaseForUser } from "./game.js";
import { getGame, getStorageDatabaseForUser } from "./game.js";
import path from "path";
import { DataRoot, RootDomain } from "./env.js";
import mime from "mime";
@ -53,8 +53,13 @@ postauthRouter.get("/cgi-bin/rpc/dollhouse_authenticate", (req, res) => {
postauthRouter.use("/cgi-bin/static", express.static(path.join(base, "injections")));
postauthRouter.get("/cgi-bin/rpc/ls/read_all", (req, res) => {
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
postauthRouter.get("/cgi-bin/rpc/storage/:name/read_all", (req, res) => {
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
res.status(400).send("Bad Request");
return;
}
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
const results = db.prepare("SELECT * FROM kv").all();
db.close();
@ -66,11 +71,19 @@ const upsertRequest = z.object({
value: z.string()
});
postauthRouter.post("/cgi-bin/rpc/ls/upsert", express.json({
limit: 1024 * 1024 * 1024 * 16
postauthRouter.post("/cgi-bin/rpc/storage/:name/upsert", express.json({
limit: 1024 * 1024 * 1024 * 16,
type: () => true
}), (req, res) => {
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
res.status(400).send("Bad Request");
return;
}
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
const body = upsertRequest.parse(req.body);
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
console.log("It is quite upserting", req.params.name, body.key);
db.prepare("INSERT INTO kv(key, value) VALUES(?, ?) ON CONFLICT (key) DO UPDATE SET value = ? WHERE key = ?").run(
body.key,
@ -87,9 +100,16 @@ const removeRequest = z.object({
key: z.string()
});
postauthRouter.post("/cgi-bin/rpc/ls/remove", express.json(), (req, res) => {
postauthRouter.post("/cgi-bin/rpc/storage/:name/remove", express.json({
type: () => true
}), (req, res) => {
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
res.status(400).send("Bad Request");
return;
}
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
const body = removeRequest.parse(req.body);
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
db.prepare("DELETE FROM kv WHERE key = ?").run(
body.key
@ -99,8 +119,13 @@ postauthRouter.post("/cgi-bin/rpc/ls/remove", express.json(), (req, res) => {
res.json({ ok: true });
});
postauthRouter.post("/cgi-bin/rpc/ls/clear", (req, res) => {
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
postauthRouter.post("/cgi-bin/rpc/storage/:name/clear", (req, res) => {
if (!(["localStorage", "sessionStorage"]).includes(req.params.name)) {
res.status(400).send("Bad Request");
return;
}
const db = getStorageDatabaseForUser(req.params.name, req.game, req.active_user);
db.prepare("DELETE FROM kv").run();
db.close();

View File

@ -2,13 +2,29 @@
// Local Storage TODOs:
// - Consistent order operation with the server
// - Compressing requests
// - websocket and firing storage events
function createStorage(name) {
const backingStore = new Map();
let keys = [];
function getKey(key) {
console.log("[LocalStorageTracker] [G]", key);
function post(endpoint, body) {
fetch("/cgi-bin/rpc/storage/" + name + "/" + endpoint, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json"
}
}).then(res => res.json()).then(r => {
if (r.ok) {
console.log("Sync ok");
} else {
console.error("Sync fucked");
}
});
}
function getKey(key) {
if (backingStore.has(key.toString())) {
return backingStore.get(key.toString());
}
@ -16,25 +32,40 @@
return null;
}
function setKey(key, value) {
console.log("[LocalStorageTracker] [S]", key, value);
function hasKey(key) {
return backingStore.has(key.toString());
}
fetch("/cgi-bin/rpc/ls/upsert", {
body: JSON.stringify({
function setKey(key, value) {
// navigator.sendBeacon("/cgi-bin/rpc/storage/" + name + "/upsert", JSON.stringify({
// key: key.toString(),
// value: value.toString()
// }))
if (name === "sessionStorage") {
try {
const xhr = new XMLHttpRequest();
xhr.open("POST", "/cgi-bin/rpc/storage/" + name + "/upsert", false);
xhr.send(JSON.stringify({
key: key.toString(),
value: value.toString()
}));
} catch(e) {
// fucking try anything idk
post("upsert", {
key: key.toString(),
value: value.toString()
}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
}).then(res => res.json()).then(r => {
if (r.ok) {
console.log("Sync ok");
} else {
console.error("Sync fucked");
}
})
navigator.sendBeacon("/cgi-bin/rpc/storage/" + name + "/upsert", JSON.stringify({
key: key.toString(),
value: value.toString()
}))
}
} else {
post("upsert", {
key: key.toString(),
value: value.toString()
})
}
if (keys.indexOf(key.toString()) === -1) {
keys.push(key.toString());
@ -44,22 +75,8 @@
}
function removeKey(key) {
console.log("[LocalStorageTracker] [R]", key);
fetch("/cgi-bin/rpc/ls/remove", {
body: JSON.stringify({
post("remove", {
key: key.toString()
}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
}).then(res => res.json()).then(r => {
if (r.ok) {
console.log("Sync ok");
} else {
console.error("Sync fucked");
}
})
if (keys.indexOf(key.toString()) !== -1) {
@ -70,21 +87,7 @@
}
function clear() {
console.log("[LocalStorageTracker] [C]");
fetch("/cgi-bin/rpc/ls/clear", {
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json"
},
method: "POST"
}).then(res => res.json()).then(r => {
if (r.ok) {
console.log("Sync ok");
} else {
console.error("Sync fucked");
}
})
post("clear", {});
keys = [];
@ -92,8 +95,6 @@
}
function getKeyByIndex(index) {
console.log("[LocalStorageTracker] [K]", index);
let realIndex = Number(index);
if (Number.isNaN(realIndex)) {
return null;
@ -117,7 +118,7 @@
}
}
Object.defineProperty(window, "localStorage", {
Object.defineProperty(window, name, {
value: new Proxy({}, {
set(_, key, value, __) {
setKey(key, value);
@ -135,15 +136,30 @@
},
ownKeys(_) {
return [...keys];
},
has(_, k) {
return hasKey(k);
},
getOwnPropertyDescriptor(_, k) {
const exists = hasKey(k);
if (!exists) return undefined;
return {
value: getKey(k),
writable: true,
enumerable: true,
configurable: true
}
},
isExtensible() {
return true;
}
}),
writable: false,
configurable: false
});
// Populate it with our initial values
const xhr = new XMLHttpRequest();
xhr.open("GET", "/cgi-bin/rpc/ls/read_all", false);
xhr.open("GET", "/cgi-bin/rpc/storage/" + name + "/read_all", false);
xhr.send(null);
if (xhr.status !== 200) {
@ -156,6 +172,15 @@
keys.push(key);
backingStore.set(key, value);
}
}
let deadline = Date.now() + 1000;
while (Date.now() < deadline){};
createStorage("localStorage");
createStorage("sessionStorage");
// DIE. DIE. DIE. (IndexedDB is hell to implement. I will do it later, for now zap it away so games fall back to localstorage.)
Object.defineProperty(window, "indexedDB", {

View File

@ -1,9 +1,9 @@
import express, { urlencoded } from "express";
import { join } from "path";
import z from "zod";
import { authenticate, sessionValidate } from "./auth.js";
import { authenticate, canCreateNewUsers, createNewUser, deleteSession, sessionValidate, updatePassword } from "./auth.js";
import { getAllGames } from "./game.js";
import { RootDomain } from "./env.js";
import { Protocol, RootDomain } from "./env.js";
const router = express.Router();
const preauthRouter = express.Router();
@ -43,13 +43,75 @@ preauthRouter.post("/", async (req, res) => {
postauthRouter.get("/", (req, res) => {
let html = `<h1>Available games</h1><ul>`;
for (let game of getAllGames()) {
html = html + `<li><a href="${req.protocol}://${game.slug}.${RootDomain}/cgi-bin/rpc/dollhouse_authenticate?sid=${req.cookies["rdh_sid"]}">${game.name}</a></li>`
html = html + `<li><a href="${Protocol}://${game.slug}.${RootDomain}/cgi-bin/rpc/dollhouse_authenticate?sid=${req.cookies["rdh_sid"]}">${game.name}</a></li>`
}
html = html + "</ul>"
if (canCreateNewUsers(req.active_user)) {
html = html + `<br><h1>Create a new user</h1><form action='/create_user' method='POST'>
<label>Username: <input type="username" name="username"></label><br/>
<label>Password: <input type="password" name="password"></label><br/>
<label>Allow new user creation: <input type="checkbox" name="allow"></label><br/>
<input type="submit">
</form>`
}
html = html + `<br><h1>Change password</h1><form action='/change_password' method='POST'>
<label>New Password: <input type="password" name="password"></label><br/>
<input type="submit">
</form>
<form action='/delete_session' method='POST'>
<input type="hidden" name="deletion" value="delete">
<input type="submit" value="Delete Session">
</form>`
res.set("Content-Type", "text/html;charset=UTF-8");
res.send(html);
});
const createPayload = z.object({
username: z.string(),
password: z.string(),
allow: z.optional(z.literal("on"))
});
postauthRouter.post('/create_user', urlencoded(), (req, res) => {
if (!canCreateNewUsers(req.active_user)) {
res.status(401).send("Unauthorized");
return;
}
const body = createPayload.parse(req.body);
const allow = body.allow === "on";
createNewUser(body.username, body.password, allow);
res.set('Location', '/');
res.status(303).send("Welcome");
});
const changePasswordPayload = z.object({
password: z.string()
});
postauthRouter.post('/change_password', urlencoded(), (req, res) => {
const body = changePasswordPayload.parse(req.body);
updatePassword(req.active_user, body.password);
res.set('Location', '/');
res.status(303).send("Welcome");
})
const deleteSessionPayload = z.object({
deletion: z.literal('delete')
});
postauthRouter.post('/delete_session', urlencoded(), (req, res) => {
deleteSessionPayload.parse(req.body);
deleteSession(req.cookies["rdh_sid"]);
res.set('Location', '/');
res.status(303).send("Welcome");
})
router.use((req, res, next) => {
const session = req.cookies["rdh_sid"];