Work
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { beforeEach, afterEach, expect, test } from "@jest/globals";
|
||||
import { unlink } from "fs/promises";
|
||||
import knex, { Knex } from "knex";
|
||||
import { Authenticator } from "./auth";
|
||||
|
||||
let db: Knex<any, unknown[]> | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = knex({
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: "test.db",
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db!.destroy();
|
||||
await unlink("test.db");
|
||||
});
|
||||
|
||||
test("Test auth", async () => {
|
||||
let auth = new Authenticator(db!);
|
||||
await auth.ensureTables();
|
||||
await auth.createAccount("admin", "admin");
|
||||
expect(await auth.verify("admin", "admin")).toBe(true);
|
||||
expect(await auth.verify("admin", "sup")).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as crypto from "crypto";
|
||||
import { Knex } from "knex";
|
||||
import { promisify } from "util";
|
||||
const pbkdf2 = promisify(crypto.pbkdf2);
|
||||
|
||||
type Account = {
|
||||
username: string;
|
||||
hashed_password: any;
|
||||
salt: any;
|
||||
};
|
||||
|
||||
export class Authenticator {
|
||||
tableName = "tokens";
|
||||
|
||||
constructor(private db: Knex<any, unknown[]>) {}
|
||||
|
||||
middleware(req: any, res: any, next: any) {
|
||||
console.log("GOing through here", req.headers.authorization);
|
||||
// if (req.headers)
|
||||
next();
|
||||
}
|
||||
|
||||
async ensureTables() {
|
||||
if (!(await this.db.schema.hasTable(this.tableName))) {
|
||||
await this.db.schema.createTable(this.tableName, (table) => {
|
||||
table.string("username");
|
||||
table.binary("hashed_password");
|
||||
table.binary("salt");
|
||||
table.primary(["username"]);
|
||||
});
|
||||
// await this.createAccount("admin", "admin");
|
||||
console.log(`Created table ${this.tableName}`);
|
||||
}
|
||||
}
|
||||
|
||||
async createAccount(username: string, password: string) {
|
||||
var salt = crypto.randomBytes(16);
|
||||
let encryptedPassword = await pbkdf2(password, salt, 310000, 32, "sha256");
|
||||
await this.db<Account>(this.tableName).insert({
|
||||
username,
|
||||
hashed_password: encryptedPassword,
|
||||
salt,
|
||||
});
|
||||
}
|
||||
|
||||
async updatePassword(username: string, password: string) {
|
||||
var salt = crypto.randomBytes(16);
|
||||
let encryptedPassword = await pbkdf2(password, salt, 310000, 32, "sha256");
|
||||
await this.db<Account>(this.tableName).update({
|
||||
username,
|
||||
hashed_password: encryptedPassword,
|
||||
salt,
|
||||
});
|
||||
}
|
||||
|
||||
async verify(username: string, password: string): Promise<boolean> {
|
||||
let users = await this.db<Account>(this.tableName).where({ username });
|
||||
if (users.length === 0) {
|
||||
throw new Error(`No such user: ${username}`);
|
||||
}
|
||||
let user = users[0];
|
||||
let encryptedPassword = await pbkdf2(
|
||||
password,
|
||||
user.salt,
|
||||
310000,
|
||||
32,
|
||||
"sha256"
|
||||
);
|
||||
return crypto.timingSafeEqual(user.hashed_password, encryptedPassword);
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,26 @@ import { EventedSpacePrimitives } from "@silverbulletmd/common/spaces/evented_sp
|
||||
import { Space } from "@silverbulletmd/common/spaces/space";
|
||||
import { createSandbox } from "@plugos/plugos/environments/node_sandbox";
|
||||
import { jwtSyscalls } from "@plugos/plugos/syscalls/jwt";
|
||||
import buildMarkdown from "@silverbulletmd/web/parser";
|
||||
import { loadMarkdownExtensions } from "@silverbulletmd/web/markdown_ext";
|
||||
import buildMarkdown from "@silverbulletmd/common/parser";
|
||||
import { loadMarkdownExtensions } from "@silverbulletmd/common/markdown_ext";
|
||||
import http, { Server } from "http";
|
||||
import { esbuildSyscalls } from "@plugos/plugos/syscalls/esbuild";
|
||||
import { systemSyscalls } from "./syscalls/system";
|
||||
import { plugPrefix } from "@silverbulletmd/common/spaces/constants";
|
||||
|
||||
import { Authenticator } from "./auth";
|
||||
import { nextTick } from "process";
|
||||
|
||||
const safeFilename = /^[a-zA-Z0-9_\-\.]+$/;
|
||||
|
||||
export type ServerOptions = {
|
||||
port: number;
|
||||
pagesPath: string;
|
||||
distDir: string;
|
||||
builtinPlugDir: string;
|
||||
preloadedModules: string[];
|
||||
token?: string;
|
||||
};
|
||||
export class ExpressServer {
|
||||
app: Express;
|
||||
system: System<SilverBulletHooks>;
|
||||
@@ -37,27 +50,23 @@ export class ExpressServer {
|
||||
private server?: Server;
|
||||
builtinPlugDir: string;
|
||||
preloadedModules: string[];
|
||||
token?: string;
|
||||
|
||||
constructor(
|
||||
port: number,
|
||||
pagesPath: string,
|
||||
distDir: string,
|
||||
builtinPlugDir: string,
|
||||
preloadedModules: string[]
|
||||
) {
|
||||
this.port = port;
|
||||
constructor(options: ServerOptions) {
|
||||
this.port = options.port;
|
||||
this.app = express();
|
||||
this.builtinPlugDir = builtinPlugDir;
|
||||
this.distDir = distDir;
|
||||
this.builtinPlugDir = options.builtinPlugDir;
|
||||
this.distDir = options.distDir;
|
||||
this.system = new System<SilverBulletHooks>("server");
|
||||
this.preloadedModules = preloadedModules;
|
||||
this.preloadedModules = options.preloadedModules;
|
||||
this.token = options.token;
|
||||
|
||||
// Setup system
|
||||
this.eventHook = new EventHook();
|
||||
this.system.addHook(this.eventHook);
|
||||
this.space = new Space(
|
||||
new EventedSpacePrimitives(
|
||||
new DiskSpacePrimitives(pagesPath),
|
||||
new DiskSpacePrimitives(options.pagesPath),
|
||||
this.eventHook
|
||||
),
|
||||
true
|
||||
@@ -65,27 +74,33 @@ export class ExpressServer {
|
||||
this.db = knex({
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: path.join(pagesPath, "data.db"),
|
||||
filename: path.join(options.pagesPath, "data.db"),
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
|
||||
this.system.registerSyscalls(["shell"], shellSyscalls(pagesPath));
|
||||
this.system.registerSyscalls(["shell"], shellSyscalls(options.pagesPath));
|
||||
this.system.addHook(new NodeCronHook());
|
||||
|
||||
this.system.registerSyscalls([], pageIndexSyscalls(this.db));
|
||||
this.system.registerSyscalls([], spaceSyscalls(this.space));
|
||||
this.system.registerSyscalls([], eventSyscalls(this.eventHook));
|
||||
this.system.registerSyscalls([], markdownSyscalls(buildMarkdown([])));
|
||||
this.system.registerSyscalls([], esbuildSyscalls());
|
||||
this.system.registerSyscalls([], systemSyscalls(this));
|
||||
this.system.registerSyscalls([], jwtSyscalls());
|
||||
this.system.registerSyscalls(
|
||||
[],
|
||||
pageIndexSyscalls(this.db),
|
||||
spaceSyscalls(this.space),
|
||||
eventSyscalls(this.eventHook),
|
||||
markdownSyscalls(buildMarkdown([])),
|
||||
esbuildSyscalls(),
|
||||
systemSyscalls(this),
|
||||
jwtSyscalls()
|
||||
);
|
||||
this.system.addHook(new EndpointHook(this.app, "/_/"));
|
||||
|
||||
this.eventHook.addLocalListener(
|
||||
"get-plug:builtin",
|
||||
async (plugName: string): Promise<Manifest> => {
|
||||
// console.log("Ok, resovling a plugin", plugName);
|
||||
if (!safeFilename.test(plugName)) {
|
||||
throw new Error(`Invalid plug name: ${plugName}`);
|
||||
}
|
||||
try {
|
||||
let manifestJson = await readFile(
|
||||
path.join(this.builtinPlugDir, `${plugName}.plug.json`),
|
||||
@@ -156,9 +171,24 @@ export class ExpressServer {
|
||||
}
|
||||
|
||||
async start() {
|
||||
const tokenMiddleware: (req: any, res: any, next: any) => void = this.token
|
||||
? (req, res, next) => {
|
||||
if (req.headers.authorization === `Bearer ${this.token}`) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).send("Unauthorized");
|
||||
}
|
||||
}
|
||||
: (req, res, next) => {
|
||||
next();
|
||||
};
|
||||
|
||||
await ensurePageIndexTable(this.db);
|
||||
console.log("Setting up router");
|
||||
|
||||
let auth = new Authenticator(this.db);
|
||||
|
||||
// Serve static files (javascript, css, html)
|
||||
this.app.use("/", express.static(this.distDir));
|
||||
|
||||
let fsRouter = express.Router();
|
||||
@@ -170,8 +200,6 @@ export class ExpressServer {
|
||||
res.json([...pages]);
|
||||
});
|
||||
|
||||
fsRouter.route("/").post(bodyParser.json(), async (req, res) => {});
|
||||
|
||||
fsRouter
|
||||
.route(/\/(.+)/)
|
||||
.get(async (req, res) => {
|
||||
@@ -242,6 +270,7 @@ export class ExpressServer {
|
||||
|
||||
this.app.use(
|
||||
"/fs",
|
||||
tokenMiddleware,
|
||||
cors({
|
||||
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
|
||||
preflightContinue: true,
|
||||
@@ -251,6 +280,8 @@ export class ExpressServer {
|
||||
|
||||
let plugRouter = express.Router();
|
||||
|
||||
// TODO: This is currently only used for the indexer calls, it's potentially dangerous
|
||||
// do we need a better solution?
|
||||
plugRouter.post(
|
||||
"/:plug/syscall/:name",
|
||||
bodyParser.json(),
|
||||
@@ -277,6 +308,7 @@ export class ExpressServer {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
plugRouter.post(
|
||||
"/:plug/function/:name",
|
||||
bodyParser.json(),
|
||||
@@ -290,7 +322,6 @@ export class ExpressServer {
|
||||
return res.send(`Plug ${plugName} not found`);
|
||||
}
|
||||
try {
|
||||
console.log("Invoking", name);
|
||||
const result = await plug.invoke(name, args);
|
||||
res.status(200);
|
||||
res.send(result);
|
||||
@@ -304,6 +335,7 @@ export class ExpressServer {
|
||||
|
||||
this.app.use(
|
||||
"/plug",
|
||||
tokenMiddleware,
|
||||
cors({
|
||||
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
|
||||
preflightContinue: true,
|
||||
@@ -312,13 +344,8 @@ export class ExpressServer {
|
||||
);
|
||||
|
||||
// Fallback, serve index.html
|
||||
// let cachedIndex: string | undefined = undefined;
|
||||
this.app.get("/*", async (req, res) => {
|
||||
// if (!cachedIndex) {
|
||||
// let cachedIndex = await readFile(`${this.distDir}/index.html`, "utf8");
|
||||
// }
|
||||
res.sendFile(`${this.distDir}/index.html`, {});
|
||||
// res.status(200).header("Content-Type", "text/html").send(cachedIndex);
|
||||
});
|
||||
|
||||
this.server = http.createServer(this.app);
|
||||
|
||||
Generated
+18660
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@
|
||||
"silverbullet": "./dist/server.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "nodemon -w dist --exec 'node --enable-source-maps dist/server.js ../../pages'"
|
||||
"start": "nodemon -w dist --exec 'node --enable-source-maps dist/server/server.js --token abc ../../pages'",
|
||||
"test": "jest dist/test"
|
||||
},
|
||||
"targets": {
|
||||
"server": {
|
||||
@@ -25,6 +26,14 @@
|
||||
"@silverbulletmd/common",
|
||||
"@silverbulletmd/web"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"source": [
|
||||
"auth.test.ts"
|
||||
],
|
||||
"outputFormat": "commonjs",
|
||||
"isLibrary": true,
|
||||
"context": "node"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -12,6 +12,9 @@ let args = yargs(hideBin(process.argv))
|
||||
type: "number",
|
||||
default: 3000,
|
||||
})
|
||||
.option("token", {
|
||||
type: "string",
|
||||
})
|
||||
.parse();
|
||||
|
||||
if (!args._.length) {
|
||||
@@ -31,13 +34,14 @@ const plugDistDir = realpathSync(
|
||||
);
|
||||
console.log("Builtin plug dist dir", plugDistDir);
|
||||
|
||||
const expressServer = new ExpressServer(
|
||||
port,
|
||||
pagesPath,
|
||||
webappDistDir,
|
||||
plugDistDir,
|
||||
preloadModules
|
||||
);
|
||||
const expressServer = new ExpressServer({
|
||||
port: port,
|
||||
pagesPath: pagesPath,
|
||||
preloadedModules: preloadModules,
|
||||
distDir: webappDistDir,
|
||||
builtinPlugDir: plugDistDir,
|
||||
token: args.token,
|
||||
});
|
||||
expressServer.start().catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user