working v1
This commit is contained in:
parent
19f3e63467
commit
e51ccfa79c
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
DEFAULT_USER_PASSWORD=
|
||||||
|
ROOT_DOMAIN=
|
||||||
|
DATA_ROOT=
|
||||||
|
PORT=
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.env
|
||||||
|
data
|
1508
package-lock.json
generated
1508
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -10,8 +10,23 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/cookie-parser": "^1.4.8",
|
||||||
|
"@types/express": "^5.0.2",
|
||||||
"@types/node": "^22.15.29",
|
"@types/node": "^22.15.29",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"argon2": "^0.43.0",
|
||||||
|
"better-sqlite3": "^11.10.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
|
"compression": "^1.8.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"html-rewriter-wasm": "^0.4.1",
|
||||||
|
"mime": "^4.0.7",
|
||||||
|
"zod": "^3.25.51"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
src/auth.ts
Normal file
61
src/auth.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import * as env from "./env.js";
|
||||||
|
import path from "path";
|
||||||
|
import argon2 from "argon2";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const db = new Database(path.join(env.DataRoot, "control.db"));
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
|
||||||
|
export async function ensureDatabases() {
|
||||||
|
db.exec(`CREATE TABLE IF NOT EXISTS users(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username NOT NULL UNIQUE,
|
||||||
|
passwordHash NOT NULL,
|
||||||
|
allowNewUserCreation NOT NULL DEFAULT 0
|
||||||
|
)`);
|
||||||
|
|
||||||
|
db.prepare(`INSERT INTO users(username, passwordHash, allowNewUserCreation) VALUES(?, ?, 1)
|
||||||
|
ON CONFLICT DO NOTHING`).run("default", await argon2.hash(env.DefaultPassword, {
|
||||||
|
timeCost: 5
|
||||||
|
}));
|
||||||
|
|
||||||
|
db.exec(`CREATE TABLE IF NOT EXISTS sessions(
|
||||||
|
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(64)))),
|
||||||
|
user INTEGER NOT NULL
|
||||||
|
)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const authResponse = z.object({
|
||||||
|
passwordHash: z.string()
|
||||||
|
});
|
||||||
|
const sessionResponse = z.object({
|
||||||
|
id: z.string()
|
||||||
|
});
|
||||||
|
export async function authenticate(username: string, password: string): Promise<string|null> {
|
||||||
|
const rawrow = db.prepare("SELECT passwordHash FROM users WHERE username = ?").get(username);
|
||||||
|
if (!rawrow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = authResponse.parse(rawrow);
|
||||||
|
const valid = await argon2.verify(row.passwordHash, password);
|
||||||
|
|
||||||
|
if (!valid) return null;
|
||||||
|
|
||||||
|
const session = sessionResponse.parse(db.prepare("INSERT INTO sessions(user) VALUES(?) RETURNING id").get(username));
|
||||||
|
|
||||||
|
return session.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionUserResponse = z.object({
|
||||||
|
user: z.string()
|
||||||
|
});
|
||||||
|
export function sessionValidate(session: string): string|null {
|
||||||
|
const rawrow = db.prepare("SELECT user FROM sessions WHERE id = ?").get(session);
|
||||||
|
if (!rawrow) return null;
|
||||||
|
|
||||||
|
const row = sessionUserResponse.parse(rawrow);
|
||||||
|
return row.user;
|
||||||
|
}
|
6
src/custom.d.ts
vendored
Normal file
6
src/custom.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
export interface Request {
|
||||||
|
game: string,
|
||||||
|
active_user: string
|
||||||
|
}
|
||||||
|
}
|
22
src/env.ts
Normal file
22
src/env.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import de from "dotenv";
|
||||||
|
de.config();
|
||||||
|
|
||||||
|
if (!process.env["DEFAULT_USER_PASSWORD"] || process.env["DEFAULT_USER_PASSWORD"].length < 8) {
|
||||||
|
throw new Error("DEFAULT_USER_PASSWORD must be set and at least 8 characters long.");
|
||||||
|
}
|
||||||
|
export const DefaultPassword: string = process.env["DEFAULT_USER_PASSWORD"];
|
||||||
|
|
||||||
|
if (!process.env["ROOT_DOMAIN"]) {
|
||||||
|
throw new Error("ROOT_DOMAIN must be set");
|
||||||
|
}
|
||||||
|
export const RootDomain: string = process.env["ROOT_DOMAIN"];
|
||||||
|
|
||||||
|
if (!process.env["DATA_ROOT"]) {
|
||||||
|
throw new Error("DATA_ROOT must be set");
|
||||||
|
}
|
||||||
|
export const DataRoot: string = process.env["DATA_ROOT"];
|
||||||
|
|
||||||
|
if (!process.env["PORT"] || !parseInt(process.env["PORT"]) || parseInt(process.env["PORT"]) < 1 || parseInt(process.env["PORT"]) > 65535) {
|
||||||
|
throw new Error("PORT must be set and be an integer between 1 and 65535");
|
||||||
|
}
|
||||||
|
export const Port: number = parseInt(process.env["PORT"]);
|
59
src/game.ts
Normal file
59
src/game.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { readdirSync, readFileSync, mkdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import z from 'zod';
|
||||||
|
import { DataRoot } from './env.js';
|
||||||
|
|
||||||
|
const games_dir = readdirSync('data/games');
|
||||||
|
|
||||||
|
const manifest = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
entrypoint: z.string(),
|
||||||
|
localstorage_setup: z.union([
|
||||||
|
z.literal("immediate"),
|
||||||
|
z.literal("deferred")
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestCache: Map<string, z.infer<typeof manifest>> = new Map();
|
||||||
|
|
||||||
|
for (let game of games_dir) {
|
||||||
|
const manifestContent = manifest.parse(JSON.parse(readFileSync('data/games/' + game + '/manifest.json', 'utf-8')));
|
||||||
|
manifestCache.set(game, manifestContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gameExists(name: string): boolean {
|
||||||
|
return manifestCache.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllGames(): Array<{ slug: string } & z.infer<typeof manifest>> {
|
||||||
|
let data = [];
|
||||||
|
for (let [k, v] of manifestCache.entries()) {
|
||||||
|
data.push({
|
||||||
|
slug: k,
|
||||||
|
...v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGame(name: string): z.infer<typeof manifest> {
|
||||||
|
if (!manifestCache.has(name)) throw new Error("no game (ps5 reference)");
|
||||||
|
|
||||||
|
return manifestCache.get(name)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalStorageDatabaseForUser(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"));
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
|
||||||
|
db.exec("CREATE TABLE IF NOT EXISTS kv(key TEXT PRIMARY KEY, value TEXT)");
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
165
src/host_router.ts
Normal file
165
src/host_router.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { sessionValidate } from "./auth.js";
|
||||||
|
import "./game.js";
|
||||||
|
import { getGame, getLocalStorageDatabaseForUser } from "./game.js";
|
||||||
|
import path from "path";
|
||||||
|
import { DataRoot, RootDomain } from "./env.js";
|
||||||
|
import mime from "mime";
|
||||||
|
import fs from "fs";
|
||||||
|
import { injectStandardHeader } from "./injector.js";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const preauthRouter = express.Router();
|
||||||
|
const postauthRouter = express.Router();
|
||||||
|
|
||||||
|
const base = import.meta.dirname;
|
||||||
|
|
||||||
|
preauthRouter.get("/cgi-bin/rpc/dollhouse_authenticate", (req, res) => {
|
||||||
|
if (!req.query.sid) {
|
||||||
|
res.status(400).send("Bad Request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = sessionValidate(req.query.sid as string);
|
||||||
|
if (username === null) {
|
||||||
|
res.status(400).send("Bad Request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.cookie("rdh_sid", req.query.sid as string, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 86400 * 400 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
res.set('Location', '/cgi-bin/startup');
|
||||||
|
res.status(307).send("Welcome");
|
||||||
|
});
|
||||||
|
|
||||||
|
postauthRouter.get("/cgi-bin/startup", (req, res) => {
|
||||||
|
const game = getGame(req.game);
|
||||||
|
|
||||||
|
res.set('Location', `/${game.entrypoint}`);
|
||||||
|
res.status(307).send("off you go girl");
|
||||||
|
});
|
||||||
|
|
||||||
|
postauthRouter.get("/cgi-bin/rpc/dollhouse_authenticate", (req, res) => {
|
||||||
|
const game = getGame(req.game);
|
||||||
|
|
||||||
|
res.set('Location', `/${game.entrypoint}`);
|
||||||
|
res.status(307).send("off you go girl");
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const results = db.prepare("SELECT * FROM kv").all();
|
||||||
|
db.close();
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
const upsertRequest = z.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
postauthRouter.post("/cgi-bin/rpc/ls/upsert", express.json({
|
||||||
|
limit: 1024 * 1024 * 1024 * 16
|
||||||
|
}), (req, res) => {
|
||||||
|
const body = upsertRequest.parse(req.body);
|
||||||
|
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||||
|
|
||||||
|
db.prepare("INSERT INTO kv(key, value) VALUES(?, ?) ON CONFLICT (key) DO UPDATE SET value = ? WHERE key = ?").run(
|
||||||
|
body.key,
|
||||||
|
body.value,
|
||||||
|
body.value,
|
||||||
|
body.key
|
||||||
|
);
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
res.json({ ok: true, "or is it?": "i don't know" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeRequest = z.object({
|
||||||
|
key: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
postauthRouter.post("/cgi-bin/rpc/ls/remove", express.json(), (req, res) => {
|
||||||
|
const body = removeRequest.parse(req.body);
|
||||||
|
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||||
|
|
||||||
|
db.prepare("DELETE FROM kv WHERE key = ?").run(
|
||||||
|
body.key
|
||||||
|
);
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
postauthRouter.post("/cgi-bin/rpc/ls/clear", (req, res) => {
|
||||||
|
const db = getLocalStorageDatabaseForUser(req.game, req.active_user);
|
||||||
|
|
||||||
|
db.prepare("DELETE FROM kv").run();
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
res.json({ ok: true, "or is it?": "is it ever ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
postauthRouter.use((req, res) => {
|
||||||
|
const game = getGame(req.game);
|
||||||
|
|
||||||
|
const parsedUrl = URL.parse(req.url, `https://${RootDomain}`);
|
||||||
|
if (!parsedUrl) {
|
||||||
|
res.status(400).send("Bad Request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsedUrl.pathname === "/") {
|
||||||
|
res.set('Location', '/cgi-bin/startup');
|
||||||
|
res.status(307).send("Welcome");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pathname = decodeURI(parsedUrl.pathname);
|
||||||
|
const resolved = path.resolve("/", pathname);
|
||||||
|
const realFile = path.join(DataRoot,'games', req.game, 'data', resolved);
|
||||||
|
console.log(realFile);
|
||||||
|
|
||||||
|
if (!fs.existsSync(realFile)) {
|
||||||
|
res.status(404).send("Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set("Content-Type", mime.getType(realFile) || "application/octet-stream");
|
||||||
|
|
||||||
|
// TODO: Perform injection into the entrypoint
|
||||||
|
|
||||||
|
if (realFile.endsWith(game.entrypoint)) {
|
||||||
|
injectStandardHeader(realFile, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback path
|
||||||
|
const size = fs.statSync(realFile);
|
||||||
|
res.set("Content-Length", size.size.toString());
|
||||||
|
const stream = fs.createReadStream(realFile);
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
const session = req.cookies["rdh_sid"];
|
||||||
|
if (!session) {
|
||||||
|
return preauthRouter(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = sessionValidate(session);
|
||||||
|
if (username === null) {
|
||||||
|
return preauthRouter(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.active_user = username;
|
||||||
|
return postauthRouter(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
47
src/index.ts
47
src/index.ts
@ -1 +1,46 @@
|
|||||||
console.log('mrraow');
|
import * as env from "./env.js";
|
||||||
|
import express from "express";
|
||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "path";
|
||||||
|
import { ensureDatabases } from "./auth.js";
|
||||||
|
import RootRouter from "./root_router.js";
|
||||||
|
import HostRouter from "./host_router.js";
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
import { gameExists } from "./game.js";
|
||||||
|
|
||||||
|
|
||||||
|
console.log("[Checking structure]");
|
||||||
|
await fs.mkdir(path.join(env.DataRoot, "storage"), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(env.DataRoot, "games"), { recursive: true });
|
||||||
|
await ensureDatabases();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
let host = req.headers["host"];
|
||||||
|
if (!host) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host === env.RootDomain) {
|
||||||
|
return RootRouter(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.endsWith("." + env.RootDomain)) {
|
||||||
|
const game = host.split("." + env.RootDomain)[0];
|
||||||
|
if (gameExists(game)) {
|
||||||
|
req.game = game;
|
||||||
|
return HostRouter(req, res, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/", (req, res) => {
|
||||||
|
res.status(400).send("Invalid host.");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(env.Port);
|
167
src/injections/standard.js
Normal file
167
src/injections/standard.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
(function() {
|
||||||
|
// Local Storage TODOs:
|
||||||
|
// - Consistent order operation with the server
|
||||||
|
// - Compressing requests
|
||||||
|
|
||||||
|
const backingStore = new Map();
|
||||||
|
let keys = [];
|
||||||
|
|
||||||
|
function getKey(key) {
|
||||||
|
console.log("[LocalStorageTracker] [G]", key);
|
||||||
|
|
||||||
|
if (backingStore.has(key.toString())) {
|
||||||
|
return backingStore.get(key.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (keys.indexOf(key.toString()) === -1) {
|
||||||
|
keys.push(key.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
backingStore.set(key.toString(), value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeKey(key) {
|
||||||
|
console.log("[LocalStorageTracker] [R]", key);
|
||||||
|
|
||||||
|
fetch("/cgi-bin/rpc/ls/remove", {
|
||||||
|
body: JSON.stringify({
|
||||||
|
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) {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k === "length") {
|
||||||
|
return keys.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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", {
|
||||||
|
value: undefined,
|
||||||
|
writable: false,
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
|
})();
|
35
src/injector.ts
Normal file
35
src/injector.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { HTMLRewriter } from "html-rewriter-wasm";
|
||||||
|
import fs from "fs";
|
||||||
|
import stream from "node:stream";
|
||||||
|
|
||||||
|
export async function injectStandardHeader(filePath: string, output: stream.Writable) {
|
||||||
|
const fstream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
const webStream = stream.Readable.toWeb(fstream);
|
||||||
|
|
||||||
|
const rewriter = new HTMLRewriter((outputChunk) => {
|
||||||
|
output.write(outputChunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
rewriter.on('head', {
|
||||||
|
element(element) {
|
||||||
|
console.log(element);
|
||||||
|
element.prepend(`<script src="/cgi-bin/static/standard.js"></script>`, {
|
||||||
|
html: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader = webStream.getReader();
|
||||||
|
while(1) {
|
||||||
|
const result = await reader.read();
|
||||||
|
if (result.done) {
|
||||||
|
rewriter.end();
|
||||||
|
rewriter.free();
|
||||||
|
output.end();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriter.write(result.value);
|
||||||
|
}
|
||||||
|
}
|
69
src/root_router.ts
Normal file
69
src/root_router.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import express, { urlencoded } from "express";
|
||||||
|
import { join } from "path";
|
||||||
|
import z from "zod";
|
||||||
|
import { authenticate, sessionValidate } from "./auth.js";
|
||||||
|
import { getAllGames } from "./game.js";
|
||||||
|
import { RootDomain } from "./env.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const preauthRouter = express.Router();
|
||||||
|
const postauthRouter = express.Router();
|
||||||
|
|
||||||
|
const base = import.meta.dirname;
|
||||||
|
|
||||||
|
const loginPayload = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
password: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
preauthRouter.use(urlencoded());
|
||||||
|
preauthRouter.get("/", (req, res) => {
|
||||||
|
res.sendFile(join(base, "statics", "login.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
preauthRouter.post("/", async (req, res) => {
|
||||||
|
const body = loginPayload.parse(req.body);
|
||||||
|
const result = await authenticate(body.username, body.password);
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
res.status(403).send("Bad username or password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.cookie("rdh_sid", result, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 86400 * 400 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
res.set('Location', '/');
|
||||||
|
res.status(303).send("Welcome");
|
||||||
|
});
|
||||||
|
|
||||||
|
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 + "</ul>"
|
||||||
|
res.set("Content-Type", "text/html;charset=UTF-8");
|
||||||
|
res.send(html);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
const session = req.cookies["rdh_sid"];
|
||||||
|
if (!session) {
|
||||||
|
return preauthRouter(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = sessionValidate(session);
|
||||||
|
if (username === null) {
|
||||||
|
return preauthRouter(req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.active_user = username;
|
||||||
|
return postauthRouter(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
16
src/statics/login.html
Normal file
16
src/statics/login.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Welcome to Dollhouse</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Welcome to DollHouse.</h1>
|
||||||
|
<p>Please authenticate to your account.</p>
|
||||||
|
<form method="POST" action="/">
|
||||||
|
<label>Username: <input type="text" name="username"></label>
|
||||||
|
<label>Password: <input type="password" name="password"></label>
|
||||||
|
<input type="submit" value="Continue">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -26,9 +26,9 @@
|
|||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "esnext", /* Specify what module code is generated. */
|
"module": "nodenext", /* Specify what module code is generated. */
|
||||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user