2022-10-21 15:06:14 +00:00
|
|
|
import { Application, path, Router } from "./deps.ts";
|
2022-11-26 13:15:38 +00:00
|
|
|
import { Manifest } from "../common/manifest.ts";
|
2022-10-10 12:50:21 +00:00
|
|
|
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
|
|
|
import { EndpointHook } from "../plugos/hooks/endpoint.ts";
|
2022-10-12 09:47:13 +00:00
|
|
|
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
2022-11-26 13:15:38 +00:00
|
|
|
import { SpaceSystem } from "./space_system.ts";
|
|
|
|
import { parseYamlSettings } from "../common/util.ts";
|
2022-10-10 12:50:21 +00:00
|
|
|
|
|
|
|
export type ServerOptions = {
|
2022-12-04 05:24:06 +00:00
|
|
|
hostname: string;
|
2022-10-10 12:50:21 +00:00
|
|
|
port: number;
|
|
|
|
pagesPath: string;
|
2022-11-26 13:15:38 +00:00
|
|
|
dbPath: string;
|
2022-10-10 12:50:21 +00:00
|
|
|
assetBundle: AssetBundle;
|
2022-12-05 11:14:21 +00:00
|
|
|
user?: string;
|
|
|
|
pass?: string;
|
2022-10-10 12:50:21 +00:00
|
|
|
};
|
|
|
|
|
2022-10-19 07:52:29 +00:00
|
|
|
const staticLastModified = new Date().toUTCString();
|
2022-10-10 12:50:21 +00:00
|
|
|
|
|
|
|
export class HttpServer {
|
|
|
|
app: Application;
|
2022-11-26 13:15:38 +00:00
|
|
|
systemBoot: SpaceSystem;
|
2022-12-04 05:24:06 +00:00
|
|
|
private hostname: string;
|
2022-10-10 12:50:21 +00:00
|
|
|
private port: number;
|
2022-12-05 11:14:21 +00:00
|
|
|
user?: string;
|
2022-10-10 12:50:21 +00:00
|
|
|
settings: { [key: string]: any } = {};
|
|
|
|
abortController?: AbortController;
|
|
|
|
|
|
|
|
constructor(options: ServerOptions) {
|
2022-12-04 05:24:06 +00:00
|
|
|
this.hostname = options.hostname;
|
2022-10-10 12:50:21 +00:00
|
|
|
this.port = options.port;
|
2022-10-10 14:20:29 +00:00
|
|
|
this.app = new Application(); //{ serverConstructor: FlashServer });
|
2022-12-05 11:14:21 +00:00
|
|
|
this.user = options.user;
|
2022-11-26 13:15:38 +00:00
|
|
|
this.systemBoot = new SpaceSystem(
|
|
|
|
options.assetBundle,
|
|
|
|
options.pagesPath,
|
|
|
|
options.dbPath,
|
2022-10-10 12:50:21 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Second, for loading plug JSON files with absolute or relative (from CWD) paths
|
2022-11-26 13:15:38 +00:00
|
|
|
this.systemBoot.eventHook.addLocalListener(
|
2022-10-10 12:50:21 +00:00
|
|
|
"get-plug:file",
|
|
|
|
async (plugPath: string): Promise<Manifest> => {
|
|
|
|
const resolvedPath = path.resolve(plugPath);
|
|
|
|
try {
|
|
|
|
const manifestJson = await Deno.readTextFile(resolvedPath);
|
|
|
|
return JSON.parse(manifestJson);
|
|
|
|
} catch {
|
|
|
|
throw new Error(
|
|
|
|
`No such file: ${resolvedPath} or could not parse as JSON`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Rescan disk every 5s to detect any out-of-process file changes
|
|
|
|
setInterval(() => {
|
2022-11-26 13:15:38 +00:00
|
|
|
this.systemBoot.space.updatePageList().catch(console.error);
|
2022-10-10 12:50:21 +00:00
|
|
|
}, 5000);
|
|
|
|
|
2022-11-26 13:15:38 +00:00
|
|
|
// Register the HTTP endpoint hook (with "/_/<plug-name>"" prefix, hardcoded for now)
|
|
|
|
this.systemBoot.system.addHook(new EndpointHook(this.app, "/_"));
|
2022-10-10 12:50:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
2022-11-26 13:15:38 +00:00
|
|
|
await this.systemBoot.start();
|
|
|
|
await this.systemBoot.ensureSpaceIndex();
|
2022-10-10 12:50:21 +00:00
|
|
|
await this.ensureAndLoadSettings();
|
2022-12-05 11:14:21 +00:00
|
|
|
|
2022-12-14 19:32:26 +00:00
|
|
|
this.addPasswordAuth(this.app);
|
|
|
|
|
2022-10-10 12:50:21 +00:00
|
|
|
// Serve static files (javascript, css, html)
|
|
|
|
this.app.use(async ({ request, response }, next) => {
|
|
|
|
if (request.url.pathname === "/") {
|
2022-10-21 14:56:46 +00:00
|
|
|
if (request.headers.get("If-Modified-Since") === staticLastModified) {
|
|
|
|
response.status = 304;
|
|
|
|
return;
|
|
|
|
}
|
2022-10-10 12:50:21 +00:00
|
|
|
response.headers.set("Content-type", "text/html");
|
2022-11-26 13:15:38 +00:00
|
|
|
response.body = this.systemBoot.assetBundle.readTextFileSync(
|
2022-10-10 12:50:21 +00:00
|
|
|
"web/index.html",
|
|
|
|
);
|
2022-10-19 07:52:29 +00:00
|
|
|
response.headers.set("Last-Modified", staticLastModified);
|
2022-10-10 12:50:21 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
const assetName = `web${request.url.pathname}`;
|
2022-10-21 14:56:46 +00:00
|
|
|
if (
|
2022-11-26 13:15:38 +00:00
|
|
|
this.systemBoot.assetBundle.has(assetName) &&
|
2022-10-21 14:56:46 +00:00
|
|
|
request.headers.get("If-Modified-Since") === staticLastModified
|
|
|
|
) {
|
|
|
|
response.status = 304;
|
|
|
|
return;
|
|
|
|
}
|
2022-10-10 12:50:21 +00:00
|
|
|
response.status = 200;
|
|
|
|
response.headers.set(
|
|
|
|
"Content-type",
|
2022-11-26 13:15:38 +00:00
|
|
|
this.systemBoot.assetBundle.getMimeType(assetName),
|
2022-10-10 12:50:21 +00:00
|
|
|
);
|
2022-11-26 13:15:38 +00:00
|
|
|
const data = this.systemBoot.assetBundle.readFileSync(
|
2022-10-12 09:47:13 +00:00
|
|
|
assetName,
|
2022-10-10 12:50:21 +00:00
|
|
|
);
|
2022-10-21 14:56:46 +00:00
|
|
|
response.headers.set("Cache-Control", "no-cache");
|
2022-10-12 09:47:13 +00:00
|
|
|
response.headers.set("Content-length", "" + data.length);
|
2022-10-19 07:52:29 +00:00
|
|
|
response.headers.set("Last-Modified", staticLastModified);
|
2022-10-12 09:47:13 +00:00
|
|
|
|
2022-10-10 12:50:21 +00:00
|
|
|
if (request.method === "GET") {
|
2022-10-12 09:47:13 +00:00
|
|
|
response.body = data;
|
2022-10-10 12:50:21 +00:00
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
await next();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Pages API
|
2022-11-26 13:15:38 +00:00
|
|
|
const fsRouter = this.buildFsRouter(this.systemBoot.spacePrimitives);
|
2022-10-10 12:50:21 +00:00
|
|
|
this.app.use(fsRouter.routes());
|
|
|
|
this.app.use(fsRouter.allowedMethods());
|
|
|
|
|
|
|
|
// Plug API
|
|
|
|
const plugRouter = this.buildPlugRouter();
|
|
|
|
this.app.use(plugRouter.routes());
|
|
|
|
this.app.use(plugRouter.allowedMethods());
|
|
|
|
|
|
|
|
// Fallback, serve index.html
|
|
|
|
this.app.use((ctx) => {
|
|
|
|
ctx.response.headers.set("Content-type", "text/html");
|
2022-11-26 13:15:38 +00:00
|
|
|
ctx.response.body = this.systemBoot.assetBundle.readTextFileSync(
|
2022-10-10 12:50:21 +00:00
|
|
|
"web/index.html",
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.abortController = new AbortController();
|
2022-12-14 19:32:26 +00:00
|
|
|
this.app.listen({
|
|
|
|
hostname: this.hostname,
|
|
|
|
port: this.port,
|
|
|
|
signal: this.abortController.signal,
|
|
|
|
})
|
2022-11-25 12:08:59 +00:00
|
|
|
.catch((e: any) => {
|
|
|
|
console.log("Server listen error:", e.message);
|
|
|
|
Deno.exit(1);
|
|
|
|
});
|
2022-12-14 19:32:26 +00:00
|
|
|
const visibleHostname = this.hostname === "0.0.0.0"
|
|
|
|
? "localhost"
|
|
|
|
: this.hostname;
|
2022-10-10 12:50:21 +00:00
|
|
|
console.log(
|
2022-12-04 05:24:06 +00:00
|
|
|
`Silver Bullet is now running: http://${visibleHostname}:${this.port}`,
|
2022-10-10 12:50:21 +00:00
|
|
|
);
|
2022-11-26 13:15:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async ensureAndLoadSettings() {
|
|
|
|
const space = this.systemBoot.space;
|
|
|
|
try {
|
|
|
|
await space.getPageMeta("SETTINGS");
|
|
|
|
} catch {
|
|
|
|
await space.writePage(
|
|
|
|
"SETTINGS",
|
|
|
|
this.systemBoot.assetBundle.readTextFileSync("SETTINGS_template.md"),
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { text: settingsText } = await space.readPage("SETTINGS");
|
|
|
|
const settings = parseYamlSettings(settingsText);
|
|
|
|
if (!settings.indexPage) {
|
|
|
|
settings.indexPage = "index";
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await space.getPageMeta(settings.indexPage);
|
|
|
|
} catch {
|
|
|
|
await space.writePage(
|
|
|
|
settings.indexPage,
|
|
|
|
`Welcome to your new space!`,
|
|
|
|
);
|
|
|
|
}
|
2022-10-10 12:50:21 +00:00
|
|
|
}
|
|
|
|
|
2022-12-05 11:14:21 +00:00
|
|
|
private addPasswordAuth(app: Application) {
|
2022-12-22 10:21:12 +00:00
|
|
|
const excludedPaths = [
|
|
|
|
"/manifest.json",
|
|
|
|
"/favicon.png",
|
|
|
|
"/logo.png",
|
|
|
|
"/.auth",
|
|
|
|
];
|
2022-12-05 11:14:21 +00:00
|
|
|
if (this.user) {
|
2022-12-22 10:21:12 +00:00
|
|
|
const b64User = btoa(this.user);
|
|
|
|
app.use(async ({ request, response, cookies }, next) => {
|
2022-12-15 11:59:31 +00:00
|
|
|
if (!excludedPaths.includes(request.url.pathname)) {
|
2022-12-22 10:21:12 +00:00
|
|
|
const authCookie = await cookies.get("auth");
|
|
|
|
if (!authCookie || authCookie !== b64User) {
|
|
|
|
response.redirect(`/.auth?refer=${request.url.pathname}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (request.url.pathname === "/.auth") {
|
|
|
|
if (request.method === "GET") {
|
|
|
|
response.headers.set("Content-type", "text/html");
|
|
|
|
response.body = this.systemBoot.assetBundle.readTextFileSync(
|
|
|
|
"web/auth.html",
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
} else if (request.method === "POST") {
|
|
|
|
const values = await request.body({ type: "form" }).value;
|
|
|
|
const username = values.get("username"),
|
|
|
|
password = values.get("password"),
|
|
|
|
refer = values.get("refer");
|
|
|
|
if (this.user === `${username}:${password}`) {
|
|
|
|
await cookies.set("auth", b64User, {
|
|
|
|
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // in a week
|
|
|
|
sameSite: "strict",
|
|
|
|
});
|
|
|
|
response.redirect(refer || "/");
|
|
|
|
// console.log("All headers", request.headers);
|
|
|
|
} else {
|
|
|
|
response.redirect("/.auth?error=1");
|
|
|
|
}
|
|
|
|
return;
|
2022-12-15 11:59:31 +00:00
|
|
|
} else {
|
|
|
|
response.status = 401;
|
|
|
|
response.body = "Unauthorized";
|
2022-12-22 10:21:12 +00:00
|
|
|
return;
|
2022-12-15 11:59:31 +00:00
|
|
|
}
|
2022-10-17 18:35:38 +00:00
|
|
|
} else {
|
2022-12-15 11:59:31 +00:00
|
|
|
// Unauthenticated access to excluded paths
|
|
|
|
await next();
|
2022-10-17 18:35:38 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private buildFsRouter(spacePrimitives: SpacePrimitives): Router {
|
|
|
|
const fsRouter = new Router();
|
|
|
|
// File list
|
|
|
|
fsRouter.get("/", async ({ response }) => {
|
|
|
|
response.headers.set("Content-type", "application/json");
|
2022-11-20 09:24:42 +00:00
|
|
|
response.body = JSON.stringify(await spacePrimitives.fetchFileList());
|
2022-10-17 18:35:38 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
fsRouter
|
2022-10-21 14:56:46 +00:00
|
|
|
.get("\/(.+)", async ({ params, response, request }) => {
|
2022-10-17 18:35:38 +00:00
|
|
|
const name = params[0];
|
2022-11-26 18:05:55 +00:00
|
|
|
// console.log("Loading file", name);
|
2022-10-17 18:35:38 +00:00
|
|
|
try {
|
|
|
|
const attachmentData = await spacePrimitives.readFile(
|
|
|
|
name,
|
|
|
|
"arraybuffer",
|
|
|
|
);
|
2022-10-21 14:56:46 +00:00
|
|
|
const lastModifiedHeader = new Date(attachmentData.meta.lastModified)
|
|
|
|
.toUTCString();
|
|
|
|
if (request.headers.get("If-Modified-Since") === lastModifiedHeader) {
|
|
|
|
response.status = 304;
|
|
|
|
return;
|
|
|
|
}
|
2022-10-17 18:35:38 +00:00
|
|
|
response.status = 200;
|
|
|
|
response.headers.set(
|
|
|
|
"X-Last-Modified",
|
|
|
|
"" + attachmentData.meta.lastModified,
|
|
|
|
);
|
2022-10-21 14:56:46 +00:00
|
|
|
response.headers.set("Cache-Control", "no-cache");
|
2022-10-17 18:35:38 +00:00
|
|
|
response.headers.set("X-Permission", attachmentData.meta.perm);
|
2022-10-21 14:56:46 +00:00
|
|
|
response.headers.set(
|
|
|
|
"Last-Modified",
|
|
|
|
lastModifiedHeader,
|
|
|
|
);
|
2022-10-17 18:35:38 +00:00
|
|
|
response.headers.set("Content-Type", attachmentData.meta.contentType);
|
|
|
|
response.body = attachmentData.data as ArrayBuffer;
|
|
|
|
} catch {
|
|
|
|
// console.error("Error in main router", e);
|
|
|
|
response.status = 404;
|
|
|
|
response.body = "";
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.put("\/(.+)", async ({ request, response, params }) => {
|
|
|
|
const name = params[0];
|
|
|
|
console.log("Saving file", name);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const meta = await spacePrimitives.writeFile(
|
|
|
|
name,
|
|
|
|
"arraybuffer",
|
|
|
|
await request.body().value,
|
|
|
|
false,
|
|
|
|
);
|
|
|
|
response.status = 200;
|
|
|
|
response.headers.set("Content-Type", meta.contentType);
|
|
|
|
response.headers.set("X-Last-Modified", "" + meta.lastModified);
|
|
|
|
response.headers.set("X-Content-Length", "" + meta.size);
|
|
|
|
response.headers.set("X-Permission", meta.perm);
|
|
|
|
response.body = "OK";
|
|
|
|
} catch (err) {
|
|
|
|
response.status = 500;
|
|
|
|
response.body = "Write failed";
|
|
|
|
console.error("Pipeline failed", err);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.options("\/(.+)", async ({ response, params }) => {
|
|
|
|
const name = params[0];
|
|
|
|
try {
|
|
|
|
const meta = await spacePrimitives.getFileMeta(name);
|
|
|
|
response.status = 200;
|
|
|
|
response.headers.set("Content-Type", meta.contentType);
|
|
|
|
response.headers.set("X-Last-Modified", "" + meta.lastModified);
|
|
|
|
response.headers.set("X-Content-Length", "" + meta.size);
|
|
|
|
response.headers.set("X-Permission", meta.perm);
|
|
|
|
} catch {
|
|
|
|
response.status = 404;
|
|
|
|
response.body = "File not found";
|
|
|
|
// console.error("Options failed", err);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.delete("\/(.+)", async ({ response, params }) => {
|
|
|
|
const name = params[0];
|
|
|
|
try {
|
|
|
|
await spacePrimitives.deleteFile(name);
|
|
|
|
response.status = 200;
|
|
|
|
response.body = "OK";
|
|
|
|
} catch (e: any) {
|
|
|
|
console.error("Error deleting attachment", e);
|
|
|
|
response.status = 200;
|
|
|
|
response.body = e.message;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return new Router().use("/fs", fsRouter.routes());
|
|
|
|
}
|
|
|
|
|
2022-10-10 12:50:21 +00:00
|
|
|
private buildPlugRouter(): Router {
|
|
|
|
const plugRouter = new Router();
|
2022-12-05 11:14:21 +00:00
|
|
|
// this.addPasswordAuth(plugRouter);
|
2022-11-26 13:15:38 +00:00
|
|
|
const system = this.systemBoot.system;
|
2022-10-10 12:50:21 +00:00
|
|
|
|
|
|
|
plugRouter.post(
|
|
|
|
"/:plug/syscall/:name",
|
|
|
|
async (ctx) => {
|
|
|
|
const name = ctx.params.name;
|
|
|
|
const plugName = ctx.params.plug;
|
|
|
|
const args = await ctx.request.body().value;
|
2022-11-26 13:15:38 +00:00
|
|
|
const plug = system.loadedPlugs.get(plugName);
|
2022-10-10 12:50:21 +00:00
|
|
|
if (!plug) {
|
|
|
|
ctx.response.status = 404;
|
|
|
|
ctx.response.body = `Plug ${plugName} not found`;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
2022-11-26 13:15:38 +00:00
|
|
|
const result = await system.syscallWithContext(
|
2022-10-10 12:50:21 +00:00
|
|
|
{ plug },
|
|
|
|
name,
|
|
|
|
args,
|
|
|
|
);
|
|
|
|
ctx.response.headers.set("Content-Type", "application/json");
|
|
|
|
ctx.response.body = JSON.stringify(result);
|
|
|
|
} catch (e: any) {
|
2022-10-14 14:49:45 +00:00
|
|
|
console.log("Error", e);
|
2022-10-10 12:50:21 +00:00
|
|
|
ctx.response.status = 500;
|
|
|
|
ctx.response.body = e.message;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
plugRouter.post(
|
|
|
|
"/:plug/function/:name",
|
|
|
|
async (ctx) => {
|
|
|
|
const name = ctx.params.name;
|
|
|
|
const plugName = ctx.params.plug;
|
|
|
|
const args = await ctx.request.body().value;
|
2022-11-26 13:15:38 +00:00
|
|
|
const plug = system.loadedPlugs.get(plugName);
|
2022-10-10 12:50:21 +00:00
|
|
|
if (!plug) {
|
|
|
|
ctx.response.status = 404;
|
|
|
|
ctx.response.body = `Plug ${plugName} not found`;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
const result = await plug.invoke(name, args);
|
|
|
|
ctx.response.headers.set("Content-Type", "application/json");
|
|
|
|
ctx.response.body = JSON.stringify(result);
|
|
|
|
} catch (e: any) {
|
|
|
|
ctx.response.status = 500;
|
|
|
|
// console.log("Error invoking function", e);
|
|
|
|
ctx.response.body = e.message;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
return new Router().use("/plug", plugRouter.routes());
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
2022-11-26 13:15:38 +00:00
|
|
|
const system = this.systemBoot.system;
|
2022-10-10 12:50:21 +00:00
|
|
|
if (this.abortController) {
|
|
|
|
console.log("Stopping");
|
2022-11-26 13:15:38 +00:00
|
|
|
await system.unloadAll();
|
2022-10-10 12:50:21 +00:00
|
|
|
console.log("Stopped plugs");
|
|
|
|
this.abortController.abort();
|
|
|
|
console.log("stopped server");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|