From 8a943500d97598a0b49ef655dc1b5484fe5e83d8 Mon Sep 17 00:00:00 2001 From: Alexis Hovorka Date: Wed, 2 Feb 2022 00:06:53 -0700 Subject: Initial commit --- lib/router.js | 53 ++++++++++++++++++++++++++++++++++++++++ lib/static.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/utils.js | 17 +++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 lib/router.js create mode 100644 lib/static.js create mode 100644 lib/utils.js (limited to 'lib') 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(`
Error getting the file: ${e}
`); + 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); }; -- cgit v1.2.3-70-g09d2