From 0278a785d3ae63117215050899ac4b053bfe3e55 Mon Sep 17 00:00:00 2001 From: Alexis Hovorka Date: Sun, 9 Jan 2022 19:53:09 -0700 Subject: Initial commit --- app/.gitignore | 49 +++++ app/LICENSE | 21 ++ app/app.js | 50 +++++ app/lib/otp.js | 29 +++ app/lib/pipe.js | 27 +++ app/lib/router.js | 40 ++++ app/lib/socket.js | 70 +++++++ app/lib/static.js | 78 +++++++ app/node-server.service | 13 ++ app/package-lock.json | 444 ++++++++++++++++++++++++++++++++++++++++ app/package.json | 20 ++ app/public/404.html | 1 + app/public/500.html | 1 + app/public/index.html | 64 ++++++ app/public/main.js | 18 ++ app/public/manifest.webmanifest | 26 +++ app/public/sock.js | 43 ++++ app/public/style.css | 14 ++ app/public/sw1.js | 29 +++ app/public/sw2.js | 40 ++++ 20 files changed, 1077 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/LICENSE create mode 100644 app/app.js create mode 100644 app/lib/otp.js create mode 100644 app/lib/pipe.js create mode 100644 app/lib/router.js create mode 100644 app/lib/socket.js create mode 100644 app/lib/static.js create mode 100644 app/node-server.service create mode 100644 app/package-lock.json create mode 100644 app/package.json create mode 100644 app/public/404.html create mode 100644 app/public/500.html create mode 100644 app/public/index.html create mode 100644 app/public/main.js create mode 100644 app/public/manifest.webmanifest create mode 100644 app/public/sock.js create mode 100644 app/public/style.css create mode 100644 app/public/sw1.js create mode 100644 app/public/sw2.js (limited to 'app') diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..bfd1621 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,49 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock +*.sock + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +users +private diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..e30e62b --- /dev/null +++ b/app/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Alexis Hovorka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/app.js b/app/app.js new file mode 100644 index 0000000..d9cd39c --- /dev/null +++ b/app/app.js @@ -0,0 +1,50 @@ +"use strict"; + +const http = require("http"); +const fs = require("fs"); + +const argon2 = require("argon2"); + +const Router = require("./lib/router"); +const Static = require("./lib/static"); +//const Socket = require("./lib/socket"); +//const pipe = require("./lib/pipe"); +//const otp = require("./lib/otp"); + +if (!fs.existsSync("./logs")) fs.mkdirSync("./logs"); +if (!fs.existsSync("./users")) fs.mkdirSync("./users"); +if (!fs.existsSync("./private")) fs.mkdirSync("./private"); + +Math.clamp = Math.clamp || ((x,l,h) => Math.max(l,Math.min(x,h))); +const PORT = Math.clamp(+process.env.PORT||8080, 1, 65535); +const HOST = process.env.HOST||"0.0.0.0"; + +const server = http.createServer(); +const stat = new Static("./public"); +//const wss = new Socket(server); +const app = new Router(); + +const cors = fn => (req, res, ...rest) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST"); + fn(req, res, ...rest); }; + +const sj = (res, data) => { + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(data)); }; + +//app.get("/", (req, res) => { +// res.writeHead(302, {"Location":"https://alexishovorka.com/"}); +// res.end(); +//}); + +//await argon2.hash(password, {type: argon2.argon2id}); +//await argon2.verify(hash, password); + +server.on("request", (req, res) => { + console.log(`${Date.now()} ${req.method} ${req.url}`); + app.route(req, res) || stat.route(req, res); +}); + +server.listen(PORT, HOST); +console.log(`${Date.now()} Running on http://${HOST}:${PORT}`); diff --git a/app/lib/otp.js b/app/lib/otp.js new file mode 100644 index 0000000..751edf4 --- /dev/null +++ b/app/lib/otp.js @@ -0,0 +1,29 @@ +"use strict"; + +const { createHmac } = require("crypto"); +const getHMAC = (k,c) => { const h = createHmac("sha1", k); + h.update(c, "hex"); return h.digest("hex"); } + +const h2d = s => parseInt(s, 16); +const d2h = s => Math.floor(s).toString(16).padStart(2,0); + +const b32a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +const b32u = s => Uint8Array.from(s.split("").reduce((a,c) => + a+b32a.indexOf(c.toUpperCase()).toString(2).padStart(5,0), "") + .match(/.{8}/g).map(b => parseInt(b,2))); + +module.exports = function totp(secret, { expiry=30, + now=Math.round(new Date().getTime()/1000), length=6 }={}) { + const time = d2h(now/expiry).padStart(16,0); + const hmac = getHMAC(b32u(secret), time); + const offset = h2d(hmac.substring(hmac.length - 1)); + const otp = (h2d(hmac.substr(2*offset, 8)) & h2d("7fffffff")) + ""; + return otp.padStart(length,0).substr(-length); +} + +module.exports.check = function check(token, secret, { expiry=30, + now=Math.round(new Date().getTime()/1000), length=6, window=0 }={}) { + const i = Array(window*2+1).fill().map((e,i) => now+(expiry*(i-window))) + .findIndex(n => token === this(secret, {now:n, expiry, length})); + return i>=0 && i-window; +} diff --git a/app/lib/pipe.js b/app/lib/pipe.js new file mode 100644 index 0000000..e329b2b --- /dev/null +++ b/app/lib/pipe.js @@ -0,0 +1,27 @@ +"use strict"; + +const { spawn } = require("child_process"); + +module.exports = function pipe({ command, flags, stdin="", buffer } = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, flags); + let stdout = (buffer?[]:""); + let stderr = ""; + + child.stderr.on("data", d => stderr += d); + child.stdout.on("data", d => (buffer? + stdout.push(d):stdout+=d)); + + child.on("close", code => { + const res = { code, stderr, stdout: + (buffer?Buffer.concat(stdout):stdout) }; + + if (code) reject(res); + else resolve(res); + }); + + //child.stdin.setEncoding("utf-8"); + child.stdin.write(stdin); + child.stdin.end(); + }); +} diff --git a/app/lib/router.js b/app/lib/router.js new file mode 100644 index 0000000..8c0a2ef --- /dev/null +++ b/app/lib/router.js @@ -0,0 +1,40 @@ +"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) { // Laziness + this.post(re, this.gather(cb, 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/app/lib/socket.js b/app/lib/socket.js new file mode 100644 index 0000000..88fdd49 --- /dev/null +++ b/app/lib/socket.js @@ -0,0 +1,70 @@ +"use strict"; + +const WebSocket = require("ws"); + +class Client { + constructor(ws) { + this.ws = ws; + this.handlers = {}; + + ws.on("message", msg => { const d = JSON.parse(msg); + (this.handlers[d.type]||[]).forEach(cb => cb(d.data)); + }); + + const closecb = () => + (this.handlers["close"]||[]).forEach(cb => cb()); + + ws.on("close", closecb); + ws.on("error", closecb); + } + + on(type, cb) { + (this.handlers[type]||(this.handlers[type]=[])).push(cb); + } + + send(type, data) { + this.ws.send(JSON.stringify({type, data})); + } +} + +module.exports = class Socket { + constructor(server) { + this.wss = new WebSocket.Server({server}); + this.handlers = {}; + this.clients = []; + + this.wss.on("connection", ws => { + const client = new Client(ws); + this.clients.push(client); + + ws.on("message", msg => { const d = JSON.parse(msg); + (this.handlers[d.type]||[]).forEach(cb => cb(client, d.data)); + }); + + let pingTimeout; + const ping = () => { client.send("ping", {}); + pingTimeout = setTimeout(() => ws.terminate(), 3e4); }; + client.on("pong", () => { clearTimeout(pingTimeout); + setTimeout(ping, 1e3); }); ping(); + + const closecb = () => { + const i = this.clients.indexOf(client); if (i < 0) return; + (this.handlers["close"]||[]).forEach(cb => cb(client)); + this.clients.splice(i, 1); + } + + ws.on("close", closecb); + ws.on("error", closecb); + + (this.handlers["open"]||[]).forEach(cb => cb(client)); + }); + } + + on(type, cb) { + (this.handlers[type]||(this.handlers[type]=[])).push(cb); + } + + sendAll(type, data) { + this.clients.forEach(c => c.send(type, data)); + } +}; diff --git a/app/lib/static.js b/app/lib/static.js new file mode 100644 index 0000000..ef48ae6 --- /dev/null +++ b/app/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/app/node-server.service b/app/node-server.service new file mode 100644 index 0000000..42c1e8f --- /dev/null +++ b/app/node-server.service @@ -0,0 +1,13 @@ +[Service] +ExecStart=/usr/bin/node /srv/http/appname/app.js +Restart=always +WorkingDirectory=/srv/http/appname +StandardOutput=syslog +StandardError=syslog +SyslogIdentifier=appname +User=alexis +Group=users +Environment=PORT=8080 + +[Install] +WantedBy=multi-user.target diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..b5eb137 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,444 @@ +{ + "name": "appname", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@mapbox/node-pre-gyp": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz", + "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==", + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.5", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + } + }, + "@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "argon2": { + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz", + "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.7", + "@phc/format": "^1.0.0", + "node-addon-api": "^4.2.0", + "opencollective-postinstall": "^2.0.3" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-addon-api": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz", + "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==" + }, + "node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "signal-exit": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..0a4ce2d --- /dev/null +++ b/app/package.json @@ -0,0 +1,20 @@ +{ + "name": "notes-web", + "version": "0.0.1", + "description": "Zettelkasten-inspired note taking web app", + "author": "Alexis Hovorka ", + "license": "MIT", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "repository": { + "type": "git", + "url": "" + }, + "private": true, + "dependencies": { + "argon2": "^0.28.3", + "ws": "^7.1.0" + } +} diff --git a/app/public/404.html b/app/public/404.html new file mode 100644 index 0000000..1a43d0a --- /dev/null +++ b/app/public/404.html @@ -0,0 +1 @@ +
404 Not Found
diff --git a/app/public/500.html b/app/public/500.html new file mode 100644 index 0000000..ee07335 --- /dev/null +++ b/app/public/500.html @@ -0,0 +1 @@ +
500 Internal Server Error
diff --git a/app/public/index.html b/app/public/index.html new file mode 100644 index 0000000..3c435f5 --- /dev/null +++ b/app/public/index.html @@ -0,0 +1,64 @@ + + + + Notes + + + + + + + + + + + + + + + +

Notes

+ + + + + diff --git a/app/public/main.js b/app/public/main.js new file mode 100644 index 0000000..ff960c2 --- /dev/null +++ b/app/public/main.js @@ -0,0 +1,18 @@ +document.addEventListener("DOMContentLoaded", async () => { "use strict"; + +const secure = location.protocol === "https:"; +const $ = (s,c) => (c||document).querySelector(s); +function $$(x,y,z,a){a=(z||document).querySelectorAll(x);if(typeof y=="function")[].forEach.call(a,y);return a} +function m(a,b,c){c=document;b=c.createElement(b||"p");b.innerHTML=a.trim();for(a=c.createDocumentFragment();c=b.firstChild;)a.appendChild(c);return a.firstChild} + +//sock.init(`ws${secure?"s":""}://${location.host}/ws`); +//sock.on("hello", e => { +// console.log("hello", e); +// sock.send("world", {foo:"bar"}); +//}); + +//if ("serviceWorker" in navigator) { +// navigator.serviceWorker.register("sw.js") +// .then(() => console.log("Service worker registered")); +//} +}); diff --git a/app/public/manifest.webmanifest b/app/public/manifest.webmanifest new file mode 100644 index 0000000..9eb9f13 --- /dev/null +++ b/app/public/manifest.webmanifest @@ -0,0 +1,26 @@ +{ + "name": "Notes", + "short_name": "Notes", + "description": "Zettelkasten-inspired note taking web app", + "categories": ["productivity", "utilities"], + "lang": "en-US", + "start_url": "/", + "scope": "/", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#000000", + "icons": [ + {"type": "image/png", "sizes": "48x48", "src": "img/favicon-48.png" }, + {"type": "image/png", "sizes": "96x96", "src": "img/favicon-96.png" }, + {"type": "image/png", "sizes": "192x192", "src": "img/favicon-192.png", "purpose": "maskable any"}, + {"type": "image/png", "sizes": "256x256", "src": "img/favicon-256.png", "purpose": "maskable any"}, + {"type": "image/png", "sizes": "512x512", "src": "img/favicon-512.png", "purpose": "maskable any"}, + + {"type": "image/png", "sizes": "72x72", "src": "img/apple-touch-icon-72.png" }, + {"type": "image/png", "sizes": "144x144", "src": "img/apple-touch-icon-144.png"}, + {"type": "image/png", "sizes": "168x168", "src": "img/apple-touch-icon-168.png"} + ], + "shortcuts": [ + {"name": "New", "url": "/new"} + ] +} diff --git a/app/public/sock.js b/app/public/sock.js new file mode 100644 index 0000000..b4905e5 --- /dev/null +++ b/app/public/sock.js @@ -0,0 +1,43 @@ +const sock = (() => { "use strict"; +const refresh = () => setTimeout(() => location.reload(), 1e3); + +let ws, pingTimeout; +const prequeue = []; +const handlers = { + "reset": [refresh], + "ping": [() => { + clearTimeout(pingTimeout); + pingTimeout = setTimeout(refresh, 3e4); + sock.send("pong", {}); + }], +}; + +const sock = { + init: url => { + ws = new WebSocket(url); + ws.addEventListener("close", refresh); + ws.addEventListener("error", refresh); + ws.addEventListener("message", e => { + const d = JSON.parse(e.data); + (handlers[d.type]||[]).forEach(cb => cb(d.data)); + }); + ws.addEventListener("open", e => { + while (prequeue.length) sock.send(...prequeue.shift()); + (handlers["open"]||[]).forEach(cb => cb()); + delete handlers["open"]; + }); + }, + + on: (type, cb) => { + if (type === "open" && ws && ws.readyState === WebSocket.OPEN) cb(); + else (handlers[type]||(handlers[type]=[])).push(cb); + }, + + send: (type, data) => { + if (ws && ws.readyState === WebSocket.OPEN) + ws.send(JSON.stringify({type, data})); + else prequeue.push([type, data]); + }, +}; + +return sock })(); diff --git a/app/public/style.css b/app/public/style.css new file mode 100644 index 0000000..655e181 --- /dev/null +++ b/app/public/style.css @@ -0,0 +1,14 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Roboto", "Noto Sans", sans-serif; + transition-timing-function: ease-in-out; +} + +body { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; +} diff --git a/app/public/sw1.js b/app/public/sw1.js new file mode 100644 index 0000000..37ae5e5 --- /dev/null +++ b/app/public/sw1.js @@ -0,0 +1,29 @@ +// https://gist.github.com/adactio/3717b7da007a9363ddf21f584aae34af + +// HTML files: try the network first, then the cache. +// Other files: try the cache first, then the network. +// Both: cache a fresh version if possible. +// (beware: the cache will grow and grow; there's no cleanup) + +const cacheName = "notes-cache-v1"; + +addEventListener("fetch", fetchEvent => { + const request = fetchEvent.request; + if (request.method !== "GET") return; + fetchEvent.respondWith(async () => { + const fetchPromise = fetch(request); + fetchEvent.waitUntil(async () => { + const responseFromFetch = await fetchPromise; + const responseCopy = responseFromFetch.clone(); + const myCache = await caches.open(cacheName); + return myCache.put(request, responseCopy); + }()); + if (request.headers.get("Accept").includes("text/html")) { + try { return await fetchPromise; } + catch(error) { return caches.match(request); } + } else { + const responseFromCache = await caches.match(request); + return responseFromCache || fetchPromise; + } + }()); +}); diff --git a/app/public/sw2.js b/app/public/sw2.js new file mode 100644 index 0000000..0dabb13 --- /dev/null +++ b/app/public/sw2.js @@ -0,0 +1,40 @@ +// https://googlechrome.github.io/samples/service-worker/basic/ + +const PRECACHE = "notes-precache-v1"; +const RUNTIME = "notes-runtime"; + +const PRECACHE_URLS = [ + "./", // Alias for index.html + "index.html", + "style.css", + "main.js" +]; + +self.addEventListener("install", e => e.waitUntil( + caches.open(PRECACHE) + .then(cache => cache.addAll(PRECACHE_URLS)) + .then(self.skipWaiting()))); + +self.addEventListener("activate", e => { + const currentCaches = [PRECACHE, RUNTIME]; + e.waitUntil( // Clean up old caches + caches.keys().then(cacheNames => + cacheNames.filter(cacheName => !currentCaches.includes(cacheName)) + ).then(cachesToDelete => Promise.all(cachesToDelete.map(cacheToDelete => + caches.delete(cacheToDelete))) + ).then(() => self.clients.claim()) + ); +}); + +self.addEventListener("fetch", e => { + if (e.request.url.startsWith(self.location.origin)) { + e.respondWith(caches.match(e.request).then(cachedResponse => + cachedResponse? cachedResponse + : caches.open(RUNTIME).then(cache => + fetch(e.request).then(res => + cache.put(e.request, res.clone()).then(() => res) + ) + ) + )); + } +}); -- cgit v1.2.3-70-g09d2