Serve static files

This commit is contained in:
Starbeamrainbowlabs 2022-02-24 00:39:33 +00:00
parent b0fd916eb4
commit d5a9668e17
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
6 changed files with 1156 additions and 964 deletions

74
package-lock.json generated
View file

@ -11,18 +11,21 @@
"dependencies": {
"@ltd/j-toml": "^1.29.0",
"applause-cli": "^1.8.0",
"fork-awesome": "^1.2.0",
"jpake": "^1.0.1",
"mime": "^3.0.0",
"nexline": "^1.2.2",
"p-queue": "^7.2.0",
"p-reflect": "^3.0.0",
"p-retry": "^5.0.0",
"powahroot": "^1.1.1",
"pretty-ms": "^7.0.1",
"systeminformation": "^5.11.2",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"esbuild": "^0.14.23"
"@types/mime": "^2.0.3",
"esbuild": "^0.14.23",
"fork-awesome": "^1.2.0"
}
},
"node_modules/@ltd/j-toml": {
@ -35,6 +38,12 @@
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.0.2.tgz",
"integrity": "sha512-sCVniU+h3GcGqxOmng11BRvf9TfN9yIs8KKjB8C8d75W69cpTfZG80gau9yTx5SxF3gvHGbJhdESzzvnjtf3Og=="
},
"node_modules/@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true
},
"node_modules/@types/retry": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
@ -418,6 +427,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fork-awesome/-/fork-awesome-1.2.0.tgz",
"integrity": "sha512-MNwTBnnudMIweHfDtTY8TeR5fxIAZ2w9o8ITn5XDySqdxa4k5AH8IuAMa89RVxDxgPNlosZxqkFKN5UmHXuYSw==",
"dev": true,
"engines": {
"node": ">=0.10.3"
}
@ -474,6 +484,17 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/nexline": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/nexline/-/nexline-1.2.2.tgz",
@ -552,6 +573,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
"integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==",
"engines": {
"node": ">=6"
}
},
"node_modules/powahroot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/powahroot/-/powahroot-1.1.1.tgz",
@ -566,6 +595,20 @@
"url": "https://liberapay.com/sbrl"
}
},
"node_modules/pretty-ms": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
"integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
"dependencies": {
"parse-ms": "^2.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@ -629,6 +672,12 @@
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.0.2.tgz",
"integrity": "sha512-sCVniU+h3GcGqxOmng11BRvf9TfN9yIs8KKjB8C8d75W69cpTfZG80gau9yTx5SxF3gvHGbJhdESzzvnjtf3Og=="
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true
},
"@types/retry": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz",
@ -827,7 +876,8 @@
"fork-awesome": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fork-awesome/-/fork-awesome-1.2.0.tgz",
"integrity": "sha512-MNwTBnnudMIweHfDtTY8TeR5fxIAZ2w9o8ITn5XDySqdxa4k5AH8IuAMa89RVxDxgPNlosZxqkFKN5UmHXuYSw=="
"integrity": "sha512-MNwTBnnudMIweHfDtTY8TeR5fxIAZ2w9o8ITn5XDySqdxa4k5AH8IuAMa89RVxDxgPNlosZxqkFKN5UmHXuYSw==",
"dev": true
},
"fs-extra": {
"version": "8.1.0",
@ -875,6 +925,11 @@
"graceful-fs": "^4.1.6"
}
},
"mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="
},
"nexline": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/nexline/-/nexline-1.2.2.tgz",
@ -926,6 +981,11 @@
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.0.2.tgz",
"integrity": "sha512-sEmji9Yaq+Tw+STwsGAE56hf7gMy9p0tQfJojIAamB7WHJYJKf1qlsg9jqBWG8q9VCxKPhZaP/AcXwEoBcYQhQ=="
},
"parse-ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
"integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
},
"powahroot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/powahroot/-/powahroot-1.1.1.tgz",
@ -936,6 +996,14 @@
"nightink": "^0.1.3"
}
},
"pretty-ms": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
"integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
"requires": {
"parse-ms": "^2.1.0"
}
},
"retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",

View file

@ -21,15 +21,18 @@
"@ltd/j-toml": "^1.29.0",
"applause-cli": "^1.8.0",
"jpake": "^1.0.1",
"mime": "^3.0.0",
"nexline": "^1.2.2",
"p-queue": "^7.2.0",
"p-reflect": "^3.0.0",
"p-retry": "^5.0.0",
"powahroot": "^1.1.1",
"pretty-ms": "^7.0.1",
"systeminformation": "^5.11.2",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@types/mime": "^2.0.3",
"esbuild": "^0.14.23",
"fork-awesome": "^1.2.0"
}

View file

@ -2,12 +2,31 @@
import ServerRouter from 'powahroot/Server.mjs';
import middleware_log from './routes/middleware_log.mjs';
import middleware_catch_errors from './routes/middleware_errors.mjs';
import route_table from './routes/table.mjs';
import route_static from './routes/static.mjs';
export default function(sys) {
const router = new ServerRouter();
router.on_all(middleware_catch_errors.bind(this, sys.config.verbose));
router.on_all(middleware_log);
///
// API
///
router.get(`/api/table/:table_name`, route_table.bind(this, sys));
///
// Web interface
///
router.get(`/`, async (ctx, next) => {
ctx.params.filepath = `index.html`;
await route_static(ctx, next);
});
router.get(`/static/::filepath`, route_static);
return router;
}

View file

@ -0,0 +1,37 @@
"use strict";
import pretty_ms from 'pretty-ms';
import log from '../../../../io/NamespacedLog.mjs'; const l = log("http:request");
/**
* Handles errors thrown by handlers further down the chain.
* @param {RequestContext} context The RequestContext object.
* @param {Function} next The function to call to invoke the next middleware item
*/
async function middleware_catch_errors(verbose, context, next) {
try {
await next();
} catch(error) {
handle_error(verbose, error, context);
}
}
/**
* Handles a given error thrown by a given RequestContext.
* @param {Error} error The error that was thrown.
* @param {RequestContext} context The RequestContext from which the error was thrown.
*/
function handle_error(verbose, error, context) {
l.info(`[?ms] [${context.request.method} 503] ${context.request.connection.remoteAddress} -> ${context.request.url}`);
console.error(error.stack); // TODO: colourise this?
// TODO: Send a better error page - perhaps with an error id that's uploaded to some remote system, or the prettified stack trace & offending code encrypted with a key?
if(!verbose)
context.send.plain(503, "Error caught", "Oops! An error ocurred. Please report this to bugs@starbeamrainbowlabs.com");
else
context.send.plain(503, `*** Server Error ***\n${error.stack}\n`);
}
export default middleware_catch_errors;

View file

@ -0,0 +1,15 @@
"use strict";
import pretty_ms from 'pretty-ms';
import log from '../../../../io/NamespacedLog.mjs'; const l = log("http:request");
async function middleware_log_request(ctx, next) {
let start = new Date();
await next();
l.info(`[${pretty_ms(new Date() - start)}] [${ctx.request.method} ${ctx.response.statusCode}] ${ctx.request.connection.remoteAddress} -> ${ctx.request.url}`);
}
export default middleware_log_request;

View file

@ -0,0 +1,50 @@
"use strict";
import path from 'path';
import fs from 'fs';
import mime from 'mime';
const __dirname = import.meta.url.slice(7, import.meta.url.lastIndexOf("/"));
const root_dir = path.resolve(__dirname, "../../../../../static-dist");
function make_path_safe(path) {
return path.replace(/[^a-zA-Z0-9\-\._\/]/g, "")
.replace(/\.+/g, ".");
}
async function route_files(context, _next) {
let safe_url = make_path_safe(context.params.filepath);
let file_path = path.join(root_dir, safe_url);
try {
let info = await fs.promises.stat(file_path);
if(info && info.isFile()) {
context.response.writeHead(200, {
"content-type": mime.getType(file_path),
"content-length": info.size
});
fs.createReadStream(file_path)
.pipe(context.response);
}
else {
send_404(context, safe_url);
return;
}
} catch(error) {
if(error.code == "ENOENT") {
send_404(context, safe_url);
return;
}
else {
throw error;
}
}
}
function send_404(context, url) {
context.send.plain(404, `Error: ${url} couldn't be found.`);
}
export { route_files };
export default route_files;