summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/router.js53
-rw-r--r--lib/static.js78
-rw-r--r--lib/utils.js17
3 files changed, 148 insertions, 0 deletions
diff --git a/lib/router.js b/lib/router.js
new file mode 100644
index 0000000..fd24693
--- /dev/null
+++ b/lib/router.js
@@ -0,0 +1,53 @@
+"use strict"; // https://github.com/mixu/minimal
+
+const url = require("url");
+const degroup = path => Object.assign(path, path.groups);
+
+class Router {
+ constructor() {
+ this.routes = [];
+ }
+
+ route(req, res) {
+ const pathname = url.parse(req.url).pathname;
+ return this.routes.some(route => {
+ const isMatch = route.method === req.method && route.re.test(pathname);
+ if (isMatch) route.cb(req, res, degroup(route.re.exec(pathname)));
+ return isMatch;
+ });
+ }
+
+ gather(cb, max) { // Handle POST data
+ return (req, res, match) => {
+ let data = "";
+ req.on("data", chunk => {
+ if ((data += chunk).length > (max||1e6)) // ~1MB
+ req.connection.destroy();
+ }).on("end", () => cb(req, res, match, data));
+ };
+ }
+
+ gpost(re, cb, max) {
+ this.post(re, this.gather(cb, max));
+ }
+
+ jpost(re, cb, max) {
+ this.gpost(re, (req, res, match, data) => {
+ try {
+ data = JSON.parse(data);
+ } catch (e) {
+ res.writeHead(400, {"Content-Type": "text/plain"});
+ return res.end("400 Bad Request");
+ }
+
+ cb(req, res, match, data);
+ }, max);
+ }
+}
+
+["get", "post", "put", "delete"].forEach(method =>
+ Router.prototype[method] = function(re, cb) {
+ this.routes.push({method: method.toUpperCase(), cb,
+ re: (re instanceof RegExp)? re : new RegExp(`^${re}$`)})});
+
+module.exports = Router;
diff --git a/lib/static.js b/lib/static.js
new file mode 100644
index 0000000..ef48ae6
--- /dev/null
+++ b/lib/static.js
@@ -0,0 +1,78 @@
+"use strict";
+
+const Path = require("path");
+const url = require("url");
+const fs = require("fs");
+
+const mimeTypes = {
+ ".html": "text/html",
+ ".css": "text/css",
+ ".js": "text/javascript",
+ ".json": "application/json",
+ ".wasm": "application/wasm",
+ ".pdf": "application/pdf",
+ ".txt": "text/plain",
+ ".md": "text/markdown",
+
+ ".png": "image/png",
+ ".jpg": "image/jpg",
+ ".svg": "image/svg+xml",
+ ".gif": "image/gif",
+ ".ico": "image/x-icon",
+
+ ".wav": "audio/wav",
+ ".mp3": "audio/mpeg",
+ ".mp4": "video/mp4",
+
+ ".eot": "application/vnd.ms-fontobject",
+ ".ttf": "application/font-ttf",
+ ".woff": "application/font-woff",
+
+ ".gltf": "model/gltf+json",
+ ".glb": "model/gltf-binary",
+};
+
+module.exports = class Static {
+ constructor(root) {
+ this.root = `${root||"."}`;
+ this.e404 = fs.readFileSync(`${this.root}/404.html`);
+ this.e500 = fs.readFileSync(`${this.root}/500.html`);
+ }
+
+ route(req, res) {
+ if (req.method !== "GET") {
+ res.writeHead(405, {"Content-Type": "text/plain"});
+ res.end("405 Method Not Allowed");
+ return;
+ }
+
+ const pathname = url.parse(req.url).pathname;
+ const sane = Path.normalize(pathname).replace(/^(\.\.\/)+/, "");
+ let path = `${this.root}${sane}`; //Path.join(__dirname, sane);
+
+ fs.stat(path, (err, stats) => {
+ if (err) { if (["EACCES", "ENOENT", "EPERM"].includes(err.code)) {
+ res.writeHead(404, {"Content-Type": "text/html"});
+ return res.end(this.e404);
+ } else { console.log(err);
+ res.writeHead(500, {"Content-Type": "text/html"});
+ return res.end(this.e500);
+ }}
+
+ if (stats.isDirectory()) path += "/index.html";
+ const ext = `${Path.extname(path)}`.toLowerCase();
+ const stream = fs.createReadStream(path);
+
+ stream.on("error", e => { console.log(e);
+ res.writeHead(500, {"Content-Type": "text/html"});
+ //res.end(`<pre>Error getting the file: ${e}</pre>`);
+ res.end(this.e500);
+ });
+
+ res.setHeader("Content-Type", mimeTypes[ext]||"application/octet-stream");
+ // TODO Caching?
+
+ stream.pipe(res);
+ });
+ }
+};
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..061070e
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,17 @@
+"use strict";
+
+const ct = module.exports.ct = (res, mime, code) =>
+ res.writeHead(code||200, {"Content-Type": mime||"application/json"});
+
+module.exports.res204 = (res) => { res.statusCode = 204; res.end(); }
+module.exports.err400 = (res, msg) => { ct(res, "text/plain", 400); res.end(""+(msg||"400 Bad Request")); }
+module.exports.err401 = (res, msg) => { ct(res, "text/plain", 401); res.end(""+(msg||"401 Unauthorized")); }
+module.exports.err403 = (res, msg) => { ct(res, "text/plain", 403); res.end(""+(msg||"403 Forbidden")); }
+module.exports.err404 = (res, msg) => { ct(res, "text/plain", 404); res.end(""+(msg||"404 Not Found")); }
+module.exports.err500 = (res, msg) => { ct(res, "text/plain", 500); res.end(""+(msg||"500 Internal Server Error")); }
+module.exports.sj = (res, data, code) => { ct(res, null, code); res.end(JSON.stringify(data)); }
+
+module.exports.cors = fn => (req, res, ...rest) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST");
+ fn(req, res, ...rest); };