diff --git a/.env.example b/.env.example index 2599dab..1400073 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ DEFAULT_USER_PASSWORD= ROOT_DOMAIN= DATA_ROOT= -PORT= \ No newline at end of file +PORT= +PROTO= \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index 9d690ee..3f36df9 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -58,4 +58,35 @@ 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); } \ No newline at end of file diff --git a/src/env.ts b/src/env.ts index f98f963..4ed3f53 100644 --- a/src/env.ts +++ b/src/env.ts @@ -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"]; \ No newline at end of file diff --git a/src/game.ts b/src/game.ts index e45009d..353be58 100644 --- a/src/game.ts +++ b/src/game.ts @@ -45,12 +45,12 @@ export function getGame(name: string): z.infer { 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)"); diff --git a/src/host_router.ts b/src/host_router.ts index 9efbaf8..1de9fd4 100644 --- a/src/host_router.ts +++ b/src/host_router.ts @@ -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(); diff --git a/src/injections/standard.js b/src/injections/standard.js index 115b109..8508625 100644 --- a/src/injections/standard.js +++ b/src/injections/standard.js @@ -2,160 +2,185 @@ // Local Storage TODOs: // - Consistent order operation with the server // - Compressing requests + // - websocket and firing storage events - const backingStore = new Map(); - let keys = []; - - function getKey(key) { - console.log("[LocalStorageTracker] [G]", key); - - if (backingStore.has(key.toString())) { - return backingStore.get(key.toString()); + function createStorage(name) { + const backingStore = new Map(); + let keys = []; + + 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"); + } + }); } - return null; - } - - function setKey(key, value) { - console.log("[LocalStorageTracker] [S]", key, value); - - fetch("/cgi-bin/rpc/ls/upsert", { - body: JSON.stringify({ - 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"); + function getKey(key) { + if (backingStore.has(key.toString())) { + return backingStore.get(key.toString()); } - }) - - if (keys.indexOf(key.toString()) === -1) { - keys.push(key.toString()); + + return null; } - backingStore.set(key.toString(), value.toString()); - } + function hasKey(key) { + return backingStore.has(key.toString()); + } - function removeKey(key) { - console.log("[LocalStorageTracker] [R]", key); + 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() + }) + 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() + }) + } - fetch("/cgi-bin/rpc/ls/remove", { - body: JSON.stringify({ + if (keys.indexOf(key.toString()) === -1) { + keys.push(key.toString()); + } + + backingStore.set(key.toString(), value.toString()); + } + + function removeKey(key) { + post("remove", { key: key.toString() + }) + + if (keys.indexOf(key.toString()) !== -1) { + keys = keys.filter(k => k !== key.toString()); + } + + backingStore.delete(key); + } + + function clear() { + post("clear", {}); + + keys = []; + + backingStore.clear(); + } + + function getKeyByIndex(index) { + let realIndex = Number(index); + if (Number.isNaN(realIndex)) { + return null; + } + + if (realIndex < 0 || realIndex >= keys.length) { + return null; + } + + return keys[realIndex]; + } + + const api = { + "clear": clear, + "getItem": getKey, + "setItem": setKey, + "removeItem": removeKey, + "key": getKeyByIndex, + "__rdh_debug": function() { + console.log(keys, backingStore); + } + } + + Object.defineProperty(window, name, { + value: new Proxy({}, { + set(_, key, value, __) { + setKey(key, value); + }, + get(_, k, __) { + if (api[k]) { + return api[k]; + } + + if (k === "length") { + return keys.length; + } + + return getKey(k); + }, + 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; + } }), - 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"); - } - }) + writable: false, + configurable: false + }); - if (keys.indexOf(key.toString()) !== -1) { - keys = keys.filter(k => k !== key.toString()); - } - - backingStore.delete(key); - } - - 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"); - } - }) - - keys = []; - - backingStore.clear(); - } - - function getKeyByIndex(index) { - console.log("[LocalStorageTracker] [K]", index); - - let realIndex = Number(index); - if (Number.isNaN(realIndex)) { - return null; - } - - if (realIndex < 0 || realIndex >= keys.length) { - return null; - } - - return keys[realIndex]; - } - - const api = { - "clear": clear, - "getItem": getKey, - "setItem": setKey, - "removeItem": removeKey, - "key": getKeyByIndex, - "__rdh_debug": function() { - console.log(keys, backingStore); - } - } - - Object.defineProperty(window, "localStorage", { - value: new Proxy({}, { - set(_, key, value, __) { - setKey(key, value); - }, - get(_, k, __) { - if (api[k]) { - return api[k]; - } + const xhr = new XMLHttpRequest(); + xhr.open("GET", "/cgi-bin/rpc/storage/" + name + "/read_all", false); + xhr.send(null); - if (k === "length") { - return keys.length; - } + if (xhr.status !== 200) { + alert("it appears to be fucked"); + window.stop(); + } - return getKey(k); - }, - ownKeys(_) { - return [...keys]; - } - }), - 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.send(null); - - if (xhr.status !== 200) { - alert("it appears to be fucked"); - window.stop(); + const responseBody = JSON.parse(xhr.responseText); + for (let { key, value } of responseBody) { + keys.push(key); + backingStore.set(key, value); + } } - const responseBody = JSON.parse(xhr.responseText); - for (let { key, value } of responseBody) { - 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", { diff --git a/src/root_router.ts b/src/root_router.ts index 8079cbf..29cab10 100644 --- a/src/root_router.ts +++ b/src/root_router.ts @@ -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 = `

Available games

" + + if (canCreateNewUsers(req.active_user)) { + html = html + `

Create a new user

+
+
+
+ +
` + } + + html = html + `

Change password

+
+ +
+
+ + +
` 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"];