diff --git a/.gitignore b/.gitignore index 518c515..c47285e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ build/ # testing folders /certs + +# IDE settings +/.vscode diff --git a/classes/db.js b/classes/db.js deleted file mode 100644 index b9a119a..0000000 --- a/classes/db.js +++ /dev/null @@ -1,100 +0,0 @@ -module.exports = class DB { - special_tables = [ - 'user', - 'db_table_versions' - ] - - constructor(db, prefix) { - this.db = db - this.prefix = '_'+prefix+'_' - } - - __formatTableName = (name) => { - if (this.special_tables.includes(name)) { - return name - } - else if (name.startsWith('_')) { - return name - } - return this.prefix + name - } - - // Table modifications - createTable = (table, columns) => { - table = this.__formatTableName(table) - columns = columns.join(', ') - this.db.run(`CREATE TABLE IF NOT EXISTS ${table} (${columns})`, [], (err) => { - if (err) throw new Error(`Failed creating new table ${table}. ${err}`) - }) - } - - dropTable = (table) => { - table = this.__formatTableName(table) - this.db.run(`DROP TABLE ${table}`, [], (err) => { - if (err) throw new Error(`Failed dropping table ${table}. ${err}`) - }) - } - - // Column modifications - addColumn = (table, column, callback) => { - table = this.__formatTableName(table) - this.db.run(`ALTER TABLE ${table} ADD COLUMN ${column}`, [], (err) => { - callback(err) - }) - } - - dropColumn = (table, column, callback) => { - table = this.__formatTableName(table) - this.db.run(`ALTER TABLE ${table} DROP COLUMN ${column}`, [], (err) => { - callback(err) - }) - } - - renameColumn = (table, old_column, new_column, callback) => { - table = this.__formatTableName(table) - this.db.run(`ALTER TABLE ${table} RENAME COLUMN ${old_column} to ${new_column}`, [], (err) => { - callback(err) - }) - } - - // Standard functions - select = (table, columns, where, order_by, params, callback) => { - table = this.__formatTableName(table) - columns = columns.join(', ') - // Construct query - let query = `SELECT ${columns} FROM ${table}` - if (where) query += ` WHERE ${where}` - if (order_by) query += ` ORDER BY ${order_by}` - // Get result - this.db.all(query, params, (err, data) => { - callback(err, data) - }) - } - - insert = (table, columns, values, callback) => { - table = this.__formatTableName(table) - let columns_str = columns.join(', ') - let values_str = `$${columns.join(', $')}` - // Run query - this.db.run(`INSERT INTO ${table} (${columns_str}) VALUES (${values_str})`, values, (err) => { - callback(err) - }) - } - - update = (table, columns, where, values, callback) => { - table = this.__formatTableName(table) - columns = columns.join(', ') - // Run query - this.db.run(`UPDATE ${table} SET ${columns} WHERE ${where}`, values, (err) => { - callback(err) - }) - } - - delete = (table, where, values, callback) => { - table = this.__formatTableName(table) - // Run query - this.db.run(`DELETE FROM ${table} WHERE ${where}`, values, (err) => { - callback(err) - }) - } -} diff --git a/classes/extension.js b/classes/extension.js deleted file mode 100644 index e2320d5..0000000 --- a/classes/extension.js +++ /dev/null @@ -1,152 +0,0 @@ -module.exports = class Extension { - admin_only = false - tables = false - dependencies = [] - - constructor(global, path) { - if (this.constructor == Extension) { - throw new Error("Abstract classes can't be instantiated."); - } - // Runs after child has been constructed - setTimeout(() => { - // Init dependencies - for (const dep of this.dependencies) { - if (dep != 'fetch') { - this[dep] = global[dep] - } - else { - this['fetch'] = new global.microfetch.Fetch(path) - } - } - // Init db - if (this.tables) { - // create interface - this.db = new global.DB(global.db(), this.name) - // init tables - new ((require(`${path}tables`))(global.Tables))(global.db(), this.db, this.name) - } - }, 0) - } - - /** - * @param {Array} path The current path being visited - * @returns true if the path requires being logged in, else false - */ - requires_login(path) { - return true - } - - requires_admin(path) { - return this.admin_only - } - - handle_req(req, res) { - req.context.extension = this - return this.handle(req, res) - } - - handle(req, res) { - throw new Error(`Method 'handle()' must be implemented.`); - } - - return(res, err, location, err_code=500) { - if (err) { - res.writeHead(err_code) - return res.end() - } - let code = 200 - let args = {} - - if (location) { - code = 307 - args['Location'] = location - } - - res.writeHead(code, args) - return res.end() - } - - return_text(req, res, item) { - req.context.__render_item = this.texts[item] - this.nj.renderString( - '{% extends "layout.html" %}{% block body %}{{__render_item |safe}}{% endblock %}', - req.context, (err, data) => { - if (err) { - res.writeHead(500) - return res.end() - } - res.writeHead(200, this.content['html']) - return res.end(data) - }) - } - - return_html(req, res, item, err, err_code=500, success_code=200, headers=null) { - if (err) { - res.writeHead(err_code) - return res.end() - } - - headers = {...this.content['html'], ...headers} - - this.nj.render(this.name+'/'+item+'.html', req.context, (err, data) => { - if (err) { - res.writeHead(err_code) - return res.end() - } - res.writeHead(success_code, headers) - return res.end(data) - }) - } - - return_file(res, file) { - this.fetch.file(file, (data,filetype,err) => { - if (err) { - res.writeHead(404) - res.end() - return - } - res.writeHead(200, this.content[filetype]) - res.end(data) - }) - } - - return_data(res, data, err, headers, err_code=404) { - if (err) { - res.writeHead(err_code) - return res.end() - } - let args = {"Content-Type": "text/plain charset utf-8"} - - if (headers) { - args = headers - } - - res.writeHead(200, args) - return res.end(data) - } - - set_cookie(key, value, secure=false) { - if (secure) - return this.cookie.serialize( - key, - value, { - secure: true, - httpOnly: true - } - ) - else - return this.cookie.serialize( - key, - value - ) - } - - del_cookie(key) { - return this.cookie.serialize( - key, - '', { - expires: new Date(1) - } - ) - } -} diff --git a/classes/tables.js b/classes/tables.js deleted file mode 100644 index 7912d8f..0000000 --- a/classes/tables.js +++ /dev/null @@ -1,60 +0,0 @@ -module.exports = class Tables { - tables = {} - - constructor(raw_db, db, prefix) { - if (this.constructor == Tables) { - throw new Error("Abstract classes can't be instantiated."); - } - this.db = db - - // Runs after child has been constructed - setTimeout(() => { - raw_db.serialize(()=> { - db.select('db_table_versions', ['table_id','version'], null, null, [], (err, data) => { - if (!data) { // Should only occur on this one missing and root called first - this['db_table_versions']['0']() - } - else { - let table_versions = {} - for (const row of data) { - table_versions[row.table_id] = row.version - } - - for (var [table, latest] of Object.entries(this.tables)) { - let table_id = `_${prefix}_${table}` - if (!Object.hasOwn(table_versions, table_id)) { - console.log(`Adding table ${table_id}`) - try { - this[table][0]() - } - catch { - throw new Error(`Missing ${table} version 0`) - } - table_versions[table_id] = 0 - raw_db.run("INSERT INTO db_table_versions (table_id, version) VALUES ($table, $version)", [table_id, table_versions[table_id]], (err) => { - if (err) throw new Error(`Failed adding table version identifier. ${err}`) - console.log(`Added table ${table_id}`) - }) - } - while (table_versions[table_id] < latest) { - console.log(`Upgrading table ${table_id} to ${table_versions[table_id]+1}`) - try { - this[table][table_versions[table_id] +1]() - } - catch { - throw new Error(`Missing ${table} version ${table_versions[table_id] +1}`) - } - - table_versions[table_id]++ - raw_db.run("UPDATE db_table_versions SET version=$version WHERE table_id=$table", [table_versions[table_id], table_id], (err) => { - if (err) throw new Error(`Failed updating table version identifier. ${err}`) - console.log(`Upgraded table ${table_id} to ${table_versions[table_id]}`) - }) - } - } - } - }) - }) - }, 0) - } -} \ No newline at end of file diff --git a/config/.gitignore b/config/.gitignore index 497148d..98c6536 100644 --- a/config/.gitignore +++ b/config/.gitignore @@ -1,3 +1,3 @@ * -!*.example +!*.example.* !.gitignore diff --git a/config/config.example.ts b/config/config.example.ts new file mode 100644 index 0000000..e905891 --- /dev/null +++ b/config/config.example.ts @@ -0,0 +1,32 @@ +import { tmpdir } from 'os' + +export default { + /** Whether the server is running behind an nginx instance */ + nginx: false, + /** The domain of the instance. */ + domain: "example.com", + /** The interface to host on. Defaults to '::' (all interfaces) */ + host: "::", + /** The port to listen on for HTTP traffic. Defaults to 80 */ + http_port: 80, + /** The salt to use for encrypting the passwords. */ + salt: "PASSWORD SALT HERE!!", + /** Location of the temporary directory. Defaults to your system's default temp dir */ + tmp_dir: tmpdir(), + /** The location where the dicebear instance is hosted */ + dicebear_host: "https://api.dicebear.com/7.x/lorelei/svg", + /** The location where the client files are hosted */ + client_location: "https://github.com/keukeiland/keuknet-client/releases/latest/download/", + /** Whether connections are logged */ + logging: false, + + // Below options are only required when running standalone + /** The port to listen on for HTTPS traffic. Defaults to 443 */ + https_port: 443, + /** The path to your HTTPS certificate-set's private key. */ + private_key_path: `${import.meta.dirname}/../certs/privkey.pem`, + /** The path to your HTTPS certificate-set's certificate. */ + server_cert_path: `${import.meta.dirname}/../certs/cert.pem`, + /** The path to your HTTPS certificate-set's CA-chain. */ + ca_cert_path: `${import.meta.dirname}/../certs/ca.pem`, +} diff --git a/config/config.js.example b/config/config.js.example deleted file mode 100644 index 6cd4ef2..0000000 --- a/config/config.js.example +++ /dev/null @@ -1,30 +0,0 @@ -const {tmpdir} = require('os') - -/** Whether the server is running behind an nginx instance */ -exports.nginx = false -/** The domain of the instance. */ -exports.domain = "example.com" -/** The interface to host on. Defaults to '::' (all interfaces) */ -exports.host = "::" -/** The port to listen on for HTTP traffic. Defaults to 80 */ -exports.http_port = 80 -/** The salt to use for encrypting the passwords. */ -exports.salt = "Password salt here!" -/** Location of the temporary directory. Defaults to your system's default temp dir */ -exports.tmp_dir = tmpdir() -/** The location where the dicebear instance is hosted */ -exports.dicebear_host = "https://api.dicebear.com/7.x/lorelei/svg" -/** The location where the client files are hosted */ -exports.client_location = "https://github.com/keukeiland/keuknet-client/releases/latest/download/" -/** Whether connections are logged */ -exports.logging = false - -// Below options are only required when running standalone -/** The port to listen on for HTTPS traffic. Defaults to 443 */ -exports.https_port = 443 -/** The path to your HTTPS certificate-set's private key. */ -exports.private_key_path = `${__dirname}/../certs/privkey.pem` -/** The path to your HTTPS certificate-set's certificate. */ -exports.server_cert_path = `${__dirname}/../certs/cert.pem` -/** The path to your HTTPS certificate-set's CA-chain. */ -exports.ca_cert_path = `${__dirname}/../certs/ca.pem` diff --git a/config/texts.example.ts b/config/texts.example.ts new file mode 100644 index 0000000..3ed357a --- /dev/null +++ b/config/texts.example.ts @@ -0,0 +1,17 @@ +export default { + index: ` +

Welcome to KeukNet!

+

+ If you're new here we recommend reading the getting started guide immediately after registering.
+ It can be found top-left next to the logo.
+ Registering and logging in can be done at the top-right. +

+

+ For help or questions please contact @fizitzfux on Discord
+ or join our Discord server. +

+

+ Please do be aware of the fact that this is still very much in development and a lot of stuff will be improved with time. +

+ `, +} diff --git a/config/texts.js.example b/config/texts.js.example deleted file mode 100644 index 64ac400..0000000 --- a/config/texts.js.example +++ /dev/null @@ -1,15 +0,0 @@ -exports.index = ` -

Welcome to KeukNet!

-

- If you're new here we recommend reading the getting started guide immediately after registering.
- It can be found top-left next to the logo.
- Registering and logging in can be done at the top-right. -

-

- For help or questions please contact @fizitzfux on Discord
- or join our Discord server. -

-

- Please do be aware of the fact that this is still very much in development and a lot of stuff will be improved with time. -

-` diff --git a/config/wireguard.example.ts b/config/wireguard.example.ts new file mode 100644 index 0000000..9085b01 --- /dev/null +++ b/config/wireguard.example.ts @@ -0,0 +1,10 @@ +export default { + subnet: "fdbe:1234:abcd:1::", + subnet_mask: "/96", + dns_server: "fdbe:1234:abcd:1::1, 1.1.1.1", + endpoint: "example.com", + port: "51820", + mtu: "1420", + privkey: "PRIVATE KEY HERE!!", //generate with `wg genkey` + pubkey: "PUBLIC KEY HERE!!", //generate with `echo | wg pubkey` +} diff --git a/config/wireguard.js.example b/config/wireguard.js.example deleted file mode 100644 index 1e77970..0000000 --- a/config/wireguard.js.example +++ /dev/null @@ -1,8 +0,0 @@ -exports.subnet = "fdbe:1234:abcd:1::" -exports.subnet_mask = "/96" -exports.dns_server = "fdbe:1234:abcd:1::1, 1.1.1.1" -exports.endpoint = "example.com" -exports.port = "51820" -exports.mtu = "1420" -exports.privkey = "INVALID KEY" //generate with `wg genkey` -exports.pubkey = "" //generate with `echo | wg pubkey` diff --git a/global.js b/global.js deleted file mode 100644 index 39627fd..0000000 --- a/global.js +++ /dev/null @@ -1,44 +0,0 @@ -/* import external modules */ -const nj = require('nunjucks').configure(['www/templates','www/extensions']) - -/* import custom modules */ -const microfetch = require('./modules/microfetch') -const Extension = require('./classes/extension') -const Tables = require('./classes/tables') -const fetch = require('./modules/fetch') -const data = require('./modules/data') -const log = require('./modules/log') -const db = require('./classes/db') - -/* exports */ -exports.content = { - html:{"Content-Type": "text/html"}, - ascii:{"Content-Type": "text/plain charset us-ascii"}, - txt:{"Content-Type": "text/plain charset utf-8"}, - json:{"Content-Type": "application/json"}, - ico:{"Content-Type": "image/x-icon", "Cache-Control": "private, max-age=3600"}, - css:{"Content-Type": "text/css", "Cache-Control": "private, max-age=3600"}, - gif:{"Content-Type": "image/gif", "Cache-Control": "private, max-age=3600"}, - jpg:{"Content-Type": "image/jpeg", "Cache-Control": "private, max-age=3600"}, - js:{"Content-Type": "text/javascript", "Cache-Control": "private, max-age=3600"}, - json:{"Content-Type": "application/json"}, - png:{"Content-Type": "image/png", "Cache-Control": "private, max-age=3600"}, - md:{"Content-Type": "text/x-markdown"}, - xml:{"Content-Type": "application/xml"}, - svg:{"Content-Type": "image/svg+xml", "Cache-Control": "private, max-age=3600"}, - webmanifest:{"Content-Type": "application/manifest+json", "Cache-Control": "private, max-age=3600"}, - mp3:{"Content-Type": "audio/mpeg", "Cache-Control": "private, max-age=3600"}, - exe:{"Content-Type": "application/vnd.microsoft.portable-executable", "Cache-Control": "private, max-age=3600"}, - py:{"Content-Type": "text/x-python", "Cache-Control": "private, max-age=3600"} -} - -exports.nj = nj -exports.microfetch = microfetch -exports.Extension = Extension -exports.Tables = Tables -exports.fetch = fetch -exports.data = data -exports.log = log -exports.DB = db - -exports.db = () => {return data.db()} diff --git a/index.js b/index.js deleted file mode 100644 index 7230bce..0000000 --- a/index.js +++ /dev/null @@ -1,162 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -const cookie = require('cookie') - -// enable use of dotenv -require('dotenv').config() - -// import config values -let config = require('./config/config') -let wg_config = require('./config/wireguard') -let texts = require('./config/texts') - -// set up global context -const global = require('./global') -global.cookie = cookie -global.config = config -global.wg_config = wg_config -global.texts = texts -const {fetch, data, log} = global - -// get request handler -const handle = require('./www/index') - -// set up modules -log.init(config.logging) -fetch.init(`${__dirname}/www/static/`) -log.status("Initializing database") -data.init(`${__dirname}/data/`, config.salt, function (err) { - if (err) log.err(err) - log.status("Database initialized") - // set up request handler - handle.init(Object.freeze(global)) -}) - -// handle all requests for both HTTPS and HTTP/2 or HTTP/nginx -const requestListener = function (req, res) { - if (process.env.DEV) { - req.headers.host = config.domain - req.ip = process.env.IP - } - - req.cookies = cookie.parse(req.headers.cookie || '') - // get authorization info - req.headers.authorization ??= req.cookies.auth - // get requested host, HTTP/<=1.1 uses host, HTTP/>=2 uses :authority - req.headers.host ??= req.headers[':authority'] - - // If standalone - if (!config.nginx) { - // get requesting IP - req.ip ??= req.socket?.remoteAddress || req.connection?.remoteAddress || req.connection.socket?.remoteAddress - - // if request is not for any domain served here, act like server isn't here - if (req.headers.host != config.domain) { - log.con_err(req) - return - } - // If behind NGINX - } else { - // get requesting IP - req.ip = req.headers['x-real-ip'] || '0.0.0.0' - } - - // separate url arguments from the url itself - [req.path, args] = req.url.split('?') - - // split arguments into key:value pairs - req.args = {} - if (args) { - for (arg of args.split('&')) { - arg = arg.split('=') - req.args[arg[0]] = arg[1] - // allow authentication using argument auth= - if (req.args.auth) req.headers.authorization ??= "Basic " + req.args.auth - } - delete args - } - - // split url into path items - req.path = req.path.split('/').slice(1) - - // log the request - log.con(req) - - // wait for all data if posting - if (req.method == 'POST') { - buffer = [] - req.on('data', function(data) { - buffer.push(data) - }) - req.on('end', function() { - req.data = {raw:Buffer.concat(buffer).toString()} - req.data.raw.split('&').forEach(function (i) { - [k,v] = i.split('=') - if (k && v) { - req.data[k] = decodeURIComponent(v).replace(/\+/g,' ') - } - }) - req.post_data = req.data.post_data - // forward the request to the handler - handle.main(req, res) - }) - } - // other methods continue - else { - // forward the request to the handler - handle.main(req, res) - } -} - -// Redirect requests to HTTPS -const httpsRedirect = function (req, res) { - res.writeHead(307, {"Location": `https://${req.headers.host}${req.url}`}) - res.end() -} - - -function startServer(http, https) { - if (https) { - log.status("Fetching encryption keys") - // Private key - fetch.key(config.private_key_path, function(key, err) { - if (err) log.err("Failed fetching private key") - // Certificate - fetch.key(config.server_cert_path, function(cert, err) { - if (err) log.err("Failed fetching server certificate") - // Certificate chain - fetch.key(config.ca_cert_path, function(ca, err) { - if (err) log.err("Failed fetching CA certificate") - log.status("Encryption keys fetched") - // Start server - require('http2').createSecureServer({ - key, - cert, - ca, - allowHTTP1: true, - }, requestListener).listen( - config.https_port, - config.host, - () => log.serverStart("https", config.domain, config.host, config.https_port) - ) - }) - }) - }) - } - if (http) { - // Start server - require('http').createServer( - https ? httpsRedirect : requestListener - ).listen( - config.http_port, - config.host, - () => log.serverStart("http", config.domain, config.host, config.http_port) - ) - } -} - -startServer(true, !config.nginx) - \ No newline at end of file diff --git a/modules/data.js b/modules/data.js deleted file mode 100644 index 2df2fd2..0000000 --- a/modules/data.js +++ /dev/null @@ -1,95 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -const crypto = require('crypto') -const sqlite3 = require('sqlite3').verbose() - -var db -var salt -exports.init = function(path, saltq, callback) { - db = new sqlite3.Database(path+'db.sqlite') - salt = saltq - return callback(undefined) -} -exports.db = () => {return db} - -function __hash_pw(password) { - if (!password) return - return crypto.pbkdf2Sync(password, salt, 10000, 128, 'sha512').toString('base64') -} - -function __decrypt_auth(auth, callback) { - if (!auth) { - return callback(undefined, undefined, new Error("Quit early")) - } - // decode authentication string - data = new Buffer.from(auth.slice(6), 'base64').toString('utf-8') - // check if both name and password - if (data.startsWith(':') || data.endsWith(':')) { - return callback(undefined, undefined, new Error("Missing name or password")) - } - [name, password] = data.split(":") - - // hash password - password = __hash_pw(password) - if (!password) - return callback(undefined, undefined, new Error("Missing name or password")) - - return callback(name, password) -} - -function __exists(name, callback) { - // check if name already exists - db.get("SELECT EXISTS(SELECT 1 FROM user WHERE name=$name)", name, function (err, result) { - return callback(!!Object.values(result)[0], err) - }) -} - - -exports.addUser = function (name, password, callback) { - if (name && password) { - password = __hash_pw(password) - // Check if username is already taken - __exists(name, function (exists, err) { - if (err) return callback(err) - if (exists) return callback(new Error("Username already taken")) - // add user to db - db.run("INSERT INTO user(name,password,pfp_code) VALUES($name,$password,$pfp_code)", [name, password, 'seed='+name], function (err) { - return callback(err) - }) - }) - } else { - return callback(new Error("Missing name or password")) - } -} - - -exports.authenticate = function (auth, ip, ip_scope, callback) { - if (auth) { - // Try to get name and password - __decrypt_auth(auth, function (name, password, err) { - if (err) { - if (ip.startsWith(ip_scope)) { - // Try using IP-address if no name and password - db.get("SELECT u.* FROM user u JOIN _profile_device p ON p.user_id = u.id WHERE p.ip=$ip", ip, function (err, user) { - return callback(user, err) - }) - return - } - return callback(undefined, err) - } - // Auth using name and password - db.get("SELECT * FROM user WHERE name=$name", name, function (err, user) { - if (user) { - if (password == user.password) { - return callback(user, err) - } - } - return callback(undefined, new Error("Wrong name or password")) - }) - }) - } else { - return callback(undefined, null) - } -} diff --git a/modules/fetch.js b/modules/fetch.js deleted file mode 100644 index 6042ef6..0000000 --- a/modules/fetch.js +++ /dev/null @@ -1,52 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -// import external libraries -const fs = require('fs').promises - -// define global variables -var fileCache = {} -var root = __dirname - -// binary file-types -const bin_exts = ['png','jpg','mp3','exe'] - - -exports.init = function (path) { - root = path -} - -exports.file = function (file, callback) { - // load from cache if available - if (fileCache[file]) { - return callback(fileCache[file]) - } - console.log(`\x1b[34m>> Caching [${file}]\x1b[0m`) - // get file-type and if it's binary or text - filetype = file.slice(file.lastIndexOf('.')+1) - encoding = bin_exts.includes(filetype) ? undefined : 'utf8' - // read the file - fs.readFile(root+file, encoding) - // cache and return the data - .then(data => { - fileCache[file] = data - return callback(data) - }) - // error if file can't be read - .catch(err => { - console.error(`\x1b[31m>> Could not read [${file}]\x1b[0m`) - return callback(undefined, err) - }) -} - -exports.key = function (location, callback) { - fs.readFile(location, "utf8") - .then(data => { - callback(data) - }) - .catch(err => { - callback(undefined, err) - }) -} \ No newline at end of file diff --git a/modules/log.js b/modules/log.js deleted file mode 100644 index 82e58d8..0000000 --- a/modules/log.js +++ /dev/null @@ -1,64 +0,0 @@ -var we_logging -exports.init = function(we_log) { - we_logging = we_log -} - -function __mask_ip(ip) { - let tmp = "" - // if IPv4 - if (ip.includes('.')) { - // strip 4to6 prefix - ip = ip.substring(ip.lastIndexOf(':')+1,ip.length) - // mask ip - ip.split('.').forEach(function (quad, index) { - quad = quad.padStart(3,"0") - if (index <= 2) tmp += quad + "." - if (index == 2) tmp += "*" - }) - } - else { - // mask ip - ip.split(':').forEach(function (quad, index) { - quad = quad.padStart(4,"0") - if (index <= 3) tmp += quad + ":" - if (index == 3) tmp += "*" - }) - } - return tmp -} - -function __mask_url(url) { - return url.split('?')[0] -} - -exports.con = function(req) { - if (we_logging) { - ip = __mask_ip(req.ip) - url = __mask_url(req.url) - console.log( - `\x1b[32m [${ip}]=>'${req.method} ${url} - HTTP/${req.httpVersion} ${(req.headers['user-agent'] ?? "NULL").split(" ",1)[0]} ${req.headers.authorization? "auth" : "noauth"}\x1b[0m` - ) - } -} - -exports.con_err = function(req) { - if (we_logging) { - ip = __mask_ip(req.ip) - console.log( - `\x1b[35m DEN[${ip}]: '${req.headers.host}'\x1b[0m` - ) - } -} - -exports.status = function(msg) { - console.log(`\x1b[34m>> ${msg}\x1b[0m`) -} - -exports.err = function(err) { - console.log(`\x1b[31m>> ${err}\x1b[0m`) -} - -exports.serverStart = function(type, domain, host, port) { - console.log(`\x1b[1m${type.toUpperCase()} server running on ${type}://${domain}:${port}, interface '${host}'\n\x1b[0m`) -} diff --git a/modules/microfetch.js b/modules/microfetch.js deleted file mode 100644 index 54459a6..0000000 --- a/modules/microfetch.js +++ /dev/null @@ -1,38 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -const {readFile} = require('fs').promises -class Fetch { - constructor (root) { - this.bin_exts = ['png','jpg','mp3'] - this.cache = {} - this.root = root+'/static/' - } - - file (file, callback) { - // load from cache if available - if (this.cache[file]) { - return callback(this.cache[file]) - } - // get file-type and if it's binary or text - var filetype = file.slice(file.lastIndexOf('.')+1) - var encoding = this.bin_exts.includes(filetype) ? undefined : 'utf8' - - let location = file.at(0) == '/' ? file : this.root+file - - // read the file - readFile(location, encoding) - // cache and return the data - .then(data => { - this.cache[file] = data - return callback(data, filetype) - }) - // error if file can't be read - .catch(err => { - return callback(undefined, undefined, err) - }) - } -} -exports.Fetch = Fetch diff --git a/package-lock.json b/package-lock.json index 6ce3c33..62b0a58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,435 @@ { "name": "keuknet", - "version": "2.4.1", + "version": "3.0.0-beta-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "keuknet", - "version": "2.4.1", + "version": "3.0.0-beta-1", "license": "MPL-2.0", "dependencies": { - "cookie": "^0.6.0", + "cookie": "^1.0.2", "dotenv": "^16.4.5", + "knex": "^3.1.0", "nunjucks": "^3.2.4", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "typescript": "^5.7.3" }, "devDependencies": { - "nodemon": "^3.1.0" + "@types/cookie": "^0.6.0", + "@types/knex": "^0.15.2", + "@types/node": "^22.10.7", + "@types/nunjucks": "^3.2.6", + "tsx": "^4.19.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@gar/promisify": { @@ -61,6 +475,48 @@ "node": ">= 6" } }, + "node_modules/@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/knex": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@types/knex/-/knex-0.15.2.tgz", + "integrity": "sha512-mw8OT8v+FK0SsgDdmio2XSkEM/yLD7ybFtiqW7I65EDTlr2aZtG+p9FhryErpNJDJ2FEXgQhe3JVBG0Gh7YbvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bluebird": "*", + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/nunjucks": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz", + "integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==", + "dev": true, + "license": "MIT" + }, "node_modules/a-sync-waterfall": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", @@ -88,9 +544,9 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", "optional": true, "dependencies": { @@ -124,20 +580,6 @@ "node": ">=8" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -170,8 +612,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -193,19 +635,6 @@ ], "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -230,26 +659,13 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -304,31 +720,6 @@ "node": ">= 10" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -358,21 +749,27 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "license": "MIT" + }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=14" } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/console-control-strings": { "version": "1.1.0", @@ -382,19 +779,18 @@ "optional": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "devOptional": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -449,9 +845,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -503,6 +899,64 @@ "license": "MIT", "optional": true }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -518,19 +972,6 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -571,6 +1012,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -592,6 +1042,34 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", + "license": "MIT" + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -602,6 +1080,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "optional": true, "dependencies": { @@ -619,19 +1098,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -639,16 +1105,6 @@ "license": "ISC", "optional": true }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -656,6 +1112,18 @@ "license": "ISC", "optional": true }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -735,13 +1203,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -793,6 +1254,15 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -807,27 +1277,19 @@ "node": ">= 12" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "devOptional": true, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "hasown": "^2.0.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-fullwidth-code-point": { @@ -840,19 +1302,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -860,16 +1309,6 @@ "license": "MIT", "optional": true }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -884,6 +1323,63 @@ "license": "MIT", "optional": true }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "license": "MIT", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -941,8 +1437,8 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, "license": "ISC", + "optional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1076,19 +1572,18 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "devOptional": true, "license": "MIT" }, "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "optional": true, "engines": { @@ -1096,9 +1591,9 @@ } }, "node_modules/node-abi": { - "version": "3.62.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", - "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", + "version": "3.73.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.73.0.tgz", + "integrity": "sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -1108,13 +1603,10 @@ } }, "node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", - "license": "MIT", - "engines": { - "node": "^16 || ^18 || >= 20" - } + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" }, "node_modules/node-gyp": { "version": "8.4.1", @@ -1141,35 +1633,6 @@ "node": ">= 10.12.0" } }, - "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -1186,16 +1649,6 @@ "node": ">=6" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -1238,6 +1691,15 @@ } } }, + "node_modules/nunjucks/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1273,23 +1735,22 @@ "node": ">=0.10.0" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "license": "MIT" }, "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -1297,7 +1758,7 @@ "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", + "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", @@ -1333,17 +1794,10 @@ "node": ">=10" } }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -1379,17 +1833,55 @@ "node": ">= 6" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "devOptional": true, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "resolve": "^1.20.0" }, "engines": { - "node": ">=8.10.0" + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/retry": { @@ -1406,6 +1898,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "optional": true, "dependencies": { @@ -1446,9 +1939,9 @@ "optional": true }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1516,19 +2009,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -1660,17 +2140,16 @@ "node": ">=0.10.0" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/tar": { @@ -1691,9 +2170,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -1733,27 +2212,42 @@ "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, "engines": { - "node": ">=8.0" + "node": ">=8" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, "bin": { - "nodetouch": "bin/nodetouch.js" + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, "node_modules/tunnel-agent": { @@ -1768,10 +2262,23 @@ "node": "*" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index a33361c..0df0837 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,22 @@ { "name": "keuknet", - "version": "2.4.1", + "version": "3.0.0-beta-1", "description": "A webserver and client program for easily managing a WireGuard-network in a client-server setting.", - "main": "index.js", + "main": "src/index.ts", + "type": "module", "scripts": { - "start": "node index.js", - "dev": "sudo npx nodemon index.js -e . --ignore ./data/" + "start": "tsx src/index.ts", + "dev": "sudo tsx watch --exclude './data/**/*' --clear-screen=false ./src/index.ts" }, "author": "Keukeiland, Fizitzfux", "license": "MPL-2.0", "dependencies": { - "cookie": "^0.6.0", + "cookie": "^1.0.2", "dotenv": "^16.4.5", + "knex": "^3.1.0", "nunjucks": "^3.2.4", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "typescript": "^5.7.3" }, "repository": { "type": "git", @@ -24,6 +27,10 @@ }, "homepage": "https://keuk.net/", "devDependencies": { - "nodemon": "^3.1.0" + "@types/cookie": "^0.6.0", + "@types/knex": "^0.15.2", + "@types/node": "^22.10.7", + "@types/nunjucks": "^3.2.6", + "tsx": "^4.19.2" } } diff --git a/src/classes/extension.ts b/src/classes/extension.ts new file mode 100644 index 0000000..7eefdfe --- /dev/null +++ b/src/classes/extension.ts @@ -0,0 +1,228 @@ +import { Environment } from "nunjucks" +import { Tables } from "./tables.ts" +import { unpack } from "../util.ts" + +export abstract class ExtensionBase implements Extension { + admin_only = false + tables = false + initialized_deps: DependencyMap = new DependencyMapImpl() + name: Extension['name'] = "default_name" + title: Extension['title'] = "Default Title" + + static async init(inst: ExtensionBase, context: InitContext): Promise { + let global: any = context.modules + let path = context.path + let knex = context.knex + + inst.initialized_deps = new DependencyMapImpl(global, context) + + // Init db + if (inst.tables) { + // init tables + let tables = new ((await import(`${path}tables`)).default)(knex, inst.initialized_deps.get('Knex'), inst.name) as Tables + let result = await tables.migrate() + return result + } + } + + init: Extension['init'] = (context) => { + return ExtensionBase.init(this, context) + } + + /** + * @returns true if the path requires being logged in, else false + */ + requires_login: Extension['requires_login'] = (path) => { + return true + } + + requires_admin: Extension['requires_admin'] = (path) => { + return this.admin_only + } + + handle_req: Extension['handle_req'] = async (ctx: Context) => { + ctx.context.extension = this as unknown as Extension + return await this.handle(ctx) + } + + abstract handle: Extension['handle'] + + return: Extension['return'] = (ctx, err, location, err_code=500) => { + const {res} = ctx + if (err) { + res.writeHead(err_code) + return res.end() + } + let code = 200 + let args: any = {} + + if (location) { + code = 307 + args['Location'] = location + } + + res.writeHead(code, args) + return res.end() + } + + return_text: Extension['return_text'] = (ctx, item) => { + const {req, res} = ctx + let [texts, nj, content]: [any, Environment, any] = this.initialized_deps.massGet('texts', 'nj', 'content') + + ctx.context.__render_item = texts[item] + nj.renderString( + '{% extends "layout.html" %}{% block body %}{{__render_item |safe}}{% endblock %}', + ctx.context, (err: Error | null, data: FileData) => { + if (err) { + res.writeHead(500) + return res.end() + } + res.writeHead(200, content.HTML) + + if (data !== null) + return res.end(data) + else + return res.end() + }) + } + + return_html: Extension['return_html'] = (ctx, item, err, err_code=500, success_code=200, headers=undefined) => { + const {req, res} = ctx + let [nj, content] = this.get_dependencies('nj', 'content') + + if (err) { + res.writeHead(err_code) + return res.end() + } + + headers = {...content.HTML, ...headers} + + nj.render(this.name+'/'+item+'.html', ctx.context, (err: null|Error, data: FileData) => { + if (err) { + res.writeHead(err_code) + return res.end() + } + res.writeHead(success_code, headers) + + if (data !== null) + return res.end(data) + else + return res.end() + }) + } + + return_file: Extension['return_file'] = async (ctx, file) => { + const {res} = ctx + let [fetch, content]: [Fetch, ContentType] = this.get_dependencies('Fetch', 'content') + + const [result, err] = await fetch.file(file).then(unpack<[string, string]>) + if (err) { + res.writeHead(404) + res.end() + return + } + + const [data, filetype] = result + + // @ts-ignore 7053 + res.writeHead(200, content[filetype]) + res.end(data) + } + + return_data: Extension['return_data'] = (ctx, data, err, headers, err_code=404) => { + const {res} = ctx + + if (err) { + res.writeHead(err_code) + return res.end() + } + let args: any = {"Content-Type": "text/plain charset utf-8"} + + if (headers) + args = headers + + res.writeHead(200, args) + return res.end(data) + } + + set_cookie: Extension['set_cookie'] = (key, value, secure=false) => { + let [cookie] = this.get_dependencies('cookie') + + if (secure) + return cookie.serialize( + key, + value, { + secure: true, + httpOnly: true + } + ) + else + return cookie.serialize( + key, + value + ) + } + + del_cookie: Extension['del_cookie'] = (key) => { + let [cookie] = this.get_dependencies('cookie') + + return cookie.serialize( + key, + '', { + expires: new Date(1) + } + ) + } + + get_dependencies: Extension['get_dependencies'] = (...args) => this.initialized_deps.massGet(...args) +} + +class DependencyMapImpl implements DependencyMap { + private global: any + private context: InitContext | {} + private map = new Map() + + constructor(global?: any, context?: InitContext) { + if (global !== undefined && context !== undefined) { + this.global = global + this.context = context + } + else { + this.global = {} + this.context = {} + } + } + + forEach = this.map.forEach + has = this.map.has + + set(key: string, value: any): this { + if (!this.map.has(key)) + this.map.set(key, value) + return this + } + + get(key: string) { + if (!this.map.has(key)) { + // Assumes type of not instantiated modules to be `Function` + let dep: any + if (typeof this.global[key] === typeof Function) { + dep = new this.global[key] + dep.init(this.context) + } + else + dep = this.global[key] + + this.map.set(key, dep) + } + return this.map.get(key) + } + + massGet(...items: S): VariableSizeArray { + let result = [] as VariableSizeArray + items.forEach((v, _, __) => + result.push(this.get(v)) + ) + return result + } +} diff --git a/src/classes/tables.ts b/src/classes/tables.ts new file mode 100644 index 0000000..4c01992 --- /dev/null +++ b/src/classes/tables.ts @@ -0,0 +1,123 @@ +import { Knex as rawKnex } from "knex" +import { Knex } from "../modules.ts" + +export abstract class Tables { + raw_knex: rawKnex + knex: Knex + prefix: string + + constructor(raw_knex: rawKnex, knex: Knex, prefix: string) { + this.raw_knex = raw_knex + this.knex = knex + this.prefix = prefix + } + + migrate(): Promise { + return new Promise(async (resolve, reject) => { + let versions = this.versions(new Map()) + let migrations = this.migrations(this.knex, new Map()) + + // Serialize?? + this.raw_knex('db_table_versions') + .select('table_id', 'version') + .then(async (data) => { + let current_versions = new Map() + for (const row of data) { + current_versions.set(row.table_id, row.version) + } + + for (var [table, latest] of versions) { + let table_id: TableId | string = `_${this.prefix}_${table}` + + if (!current_versions.has(table_id)) { + console.log(`Adding table ${table_id}`) + + if (!migrations.has(table)) + return reject(`Missing entry in migrations for table '${table_id}'`) + let migration = migrations.get(table) as MigrationRecord + if (!migration[0]) + return reject(`Missing migration entry 0 for table '${table}'`) + await migration[0]() + + current_versions.set(table_id, 0) + this.raw_knex('db_table_versions') + .insert({ + table_id, + version: current_versions.get(table_id), + }) + .then( + () => { + console.log(`Added table ${table_id}`) + }, (err) => { + if (err) return reject(`Failed adding table version identifier. ${err}`) + } + ) + } + + let current_version: number + while (true) { + current_version = current_versions.get(table_id) ?? -1 + if (current_version < 0 || current_version == latest) break + let new_version = current_version +1 + + console.log(`Upgrading table ${table_id} from ${current_version} to ${new_version}`) + try { + let migration = Tables.getMigration(migrations, table, new_version) + if (migration instanceof Error) return reject(migration) + await migration() + } + catch (err) { + console.error(err) + break + } + + current_versions.set(table_id, new_version) + + this.raw_knex('db_table_versions') + .update({ + version: new_version, + }) + .whereIn('table_id', [table_id]) + .then( + () => { + console.log(`Upgraded table ${table_id} to ${new_version}`) + }, + (err) => { + return reject(`Failed updating table version identifier. ${err}`) + } + ) + } + } + }) + resolve() + }) + } + + private static getMigration(migrations: MigrationMap, table: string, version: number): Migration | Error { + if (migrations.has(table)) { + let migration = migrations.get(table) as MigrationRecord + let result = migration[version] + if (!(result === undefined)) { + return result as Migration + } + else + return new Error(`Missing migration entry ${version} for table '${table}'`) + } + else + return new Error(`Missing entry in migrations for table '${table}'`) + } + + versions(versions: VersionMap): VersionMap { + return versions + } + migrations(knex: Knex, migrations: MigrationMap): MigrationMap { + return migrations + } +} + +export type Migration = Function +export type MigrationRecord = Record +export type MigrationMap = Map +export type VersionMap = Map + +type TableId = `${string}.${string}` diff --git a/www/extensions/admin/index.html b/src/extensions/admin/index.html similarity index 100% rename from www/extensions/admin/index.html rename to src/extensions/admin/index.html diff --git a/src/extensions/admin/index.ts b/src/extensions/admin/index.ts new file mode 100644 index 0000000..b547f58 --- /dev/null +++ b/src/extensions/admin/index.ts @@ -0,0 +1,11 @@ +import { ExtensionBase } from "../../modules.ts" + +export default class extends ExtensionBase { + override name = 'admin' + override title = 'Admin' + override admin_only = true + + override handle: Extension['handle'] = (ctx) => { + this.return_html(ctx, 'index') + } +} diff --git a/www/extensions/chat/index.html b/src/extensions/chat/index.html similarity index 100% rename from www/extensions/chat/index.html rename to src/extensions/chat/index.html diff --git a/src/extensions/chat/index.ts b/src/extensions/chat/index.ts new file mode 100644 index 0000000..6876236 --- /dev/null +++ b/src/extensions/chat/index.ts @@ -0,0 +1,60 @@ +import { ExtensionBase } from "../../modules.ts" + +export default class extends ExtensionBase { + override name = 'chat' + override title = 'Chat' + + messages: {user: {name: any, pfp_code: any}, time: any, content: any}[] = [{ + user: {name:'SYSTEM',pfp_code:'seed=SYSTEM'}, + time:(new Date()).toLocaleTimeString('en-US', {hour12: false}), + content: 'Welcome to the chatroom!' + }] + last_got_id: {[user_id: number]: number} = {} + + override handle: Extension['handle'] = (ctx) => { + var location = ctx.path.shift() + + const user_id = ctx.context.user?.id as number + + if (!location) { + if (ctx.data && ctx.data.form.message) { + var message = ctx.data.form.message.substring(0,255) + var now = (new Date()).toLocaleTimeString('en-US', {hour12: false}) + this.messages.push({ + user: { + name: ctx.context.user?.name, + pfp_code: ctx.context.user?.pfp_code, + }, + time: now, + content: message, + }) + } + ctx.context.chat = this.messages + this.last_got_id[user_id] = this.messages.length + return this.return_html(ctx, 'index') + } + else if (location == 'getnew') { + var part = this.last_got_id.hasOwnProperty(user_id) ? this.last_got_id[user_id] : 0 + this.last_got_id[user_id] = this.messages.length + return this.return_data(ctx, `{"messages":${JSON.stringify(this.messages.slice(part))}}`) + } + else if (location == 'postmessage') { + if (ctx.data && ctx.data.form.message) { + var message = ctx.data.form.message.substring(0,255) + var now = (new Date()).toLocaleTimeString('en-US', {hour12: false}) + this.messages.push({ + user: { + name: ctx.context.user?.name, + pfp_code: ctx.context.user?.pfp_code, + }, + time: now, + content: message, + }) + } + return this.return(ctx) + } + else { + return this.return_file(ctx, location) + } + } +} diff --git a/www/extensions/chat/static/index.css b/src/extensions/chat/static/index.css similarity index 100% rename from www/extensions/chat/static/index.css rename to src/extensions/chat/static/index.css diff --git a/www/extensions/nothing/index.html b/src/extensions/nothing/index.html similarity index 100% rename from www/extensions/nothing/index.html rename to src/extensions/nothing/index.html diff --git a/src/extensions/nothing/index.ts b/src/extensions/nothing/index.ts new file mode 100644 index 0000000..1af0a29 --- /dev/null +++ b/src/extensions/nothing/index.ts @@ -0,0 +1,10 @@ +import { ExtensionBase } from "../../modules.ts" + +export default class extends ExtensionBase { + override name = 'nothing' + override title = 'Nothing' + + override handle: Extension['handle'] = (ctx) => { + this.return_html(ctx, 'index') + } +} diff --git a/www/extensions/profile/devices/android.html b/src/extensions/profile/devices/android.html similarity index 100% rename from www/extensions/profile/devices/android.html rename to src/extensions/profile/devices/android.html diff --git a/www/extensions/profile/devices/ios.html b/src/extensions/profile/devices/ios.html similarity index 100% rename from www/extensions/profile/devices/ios.html rename to src/extensions/profile/devices/ios.html diff --git a/www/extensions/profile/devices/linux.html b/src/extensions/profile/devices/linux.html similarity index 100% rename from www/extensions/profile/devices/linux.html rename to src/extensions/profile/devices/linux.html diff --git a/www/extensions/profile/devices/macos.html b/src/extensions/profile/devices/macos.html similarity index 100% rename from www/extensions/profile/devices/macos.html rename to src/extensions/profile/devices/macos.html diff --git a/www/extensions/profile/devices/windows.html b/src/extensions/profile/devices/windows.html similarity index 100% rename from www/extensions/profile/devices/windows.html rename to src/extensions/profile/devices/windows.html diff --git a/www/extensions/profile/edit.html b/src/extensions/profile/edit.html similarity index 100% rename from www/extensions/profile/edit.html rename to src/extensions/profile/edit.html diff --git a/www/extensions/profile/index.html b/src/extensions/profile/index.html similarity index 100% rename from www/extensions/profile/index.html rename to src/extensions/profile/index.html diff --git a/src/extensions/profile/index.ts b/src/extensions/profile/index.ts new file mode 100644 index 0000000..3a76bc2 --- /dev/null +++ b/src/extensions/profile/index.ts @@ -0,0 +1,146 @@ +import crypto from 'crypto' +import { ExtensionBase, Knex } from '../../modules.ts' +import { unpack } from '../../util.ts' + +export default class extends ExtensionBase { + override name = 'profile' + override title = 'Network' + override tables = true + wg: any = null + wg_config: any = null + + + override init: Extension['init'] = async (context) => { + let config = context.modules.config + let data_path = context.data_path + this.wg_config = context.modules.wg_config + + this.wg = await import('./wireguard.js') + this.wg.init(data_path, this.wg_config, config.tmp_dir) + + return ExtensionBase.init(this, context) + } + + override requires_login: Extension['requires_login'] = (path) => { + if (path.at(0) == 'getconf') { + return false + } + return true + } + + override handle: Extension['handle'] = async (ctx) => { + let [knex]: [Knex] = this.get_dependencies('Knex') + var location = ctx.path.shift() + + switch (location) { + case '': + case undefined: { + const [profiles, err] = await knex.query('_device') + .select('*') + .where('user_id', ctx.context.user?.id) + .then(unpack) + + ctx.context.profiles = profiles + ctx.context.connected_ip = ctx.ip.startsWith(this.wg_config.subnet) ? ctx.ip : false + return this.return_html(ctx, 'index', err) + } + case 'delete': { + // Check ownership + let user_owns = await this.owns(ctx.context.user, ctx.args.get('uuid')) + if (!user_owns) + return this.return(ctx, new Error(), undefined, 404) + + // Delete db entry + await knex.query('_device').delete().where('uuid', ctx.args.get('uuid')) + + // Delete wireguard profile + this.wg.remove(ctx.args.get('uuid'), () => { + return this.return(ctx, undefined, location='/profile') + }) + break + } + case 'add': { + // Get uuid + let uuid = crypto.randomUUID() + + // Get IP suffix + let [data, err] = await knex.query('_device') + .max('id') + .then(unpack) + let id = (data[0]['max(`id`)'] ?? 0) +2 + + // Register wireguard link + this.wg.create(uuid, id, async (ip: string, err: Error) => { + if (err) return this.return(ctx, err) + // Insert in db + await knex.query('_device').insert({user_id: ctx.context.user?.id, uuid, ip} as never) + + return this.return(ctx, undefined, location='/profile') + }) + break + } + case 'getconf': { + // Get uuid + let uuid = ctx.args.get('uuid') + + // Get config + this.wg.getConfig(uuid, async (data: FileData, err: Error) => { + if (err) + return this.return(ctx, new Error(), undefined, 404) + + // Mark as installed + await knex.query('_device') + .update({installed: true} as never) + .where('uuid', uuid) + + return this.return_data(ctx, data, undefined, {"Content-Type": "text/plain charset utf-8", "Content-Disposition": `attachment; filename="keuknet.conf"`}) + }) + break + } + case 'install': { + ctx.context.device = ctx.args.get('device') + ctx.context.uuid = ctx.args.get('uuid') + return this.return_html(ctx, 'install') + } + case 'rename': { + if (ctx.data) { + // Check ownership + let user_owns = await this.owns(ctx.context.user, ctx.args.get('uuid')) + if (!user_owns) + return this.return(ctx, new Error(), undefined, 404) + + // Change name + await knex.query('_device') + .update({name: ctx.data.form.post_data} as never) + .where('uuid', ctx.args.get('uuid')) + + return this.return(ctx, undefined, location='/profile') + } + else { + ctx.context = {...ctx.context, item:"new name",action:ctx.req.url,destination:"/profile"} + this.return_html(ctx, 'edit') + } + break + } + default: { + return this.return_file(ctx, location) + } + } + } + + owns = async (user?: User, uuid?: string) => { + let [knex]: [Knex] = this.get_dependencies('Knex') + + if (!uuid) + return false + + const [data, err] = await knex.query('_device') + .select('id') + .where('user_id', user?.id) + .andWhere('uuid', uuid) + .first() + .then(unpack) + + return !!data + } +} diff --git a/www/extensions/profile/install.html b/src/extensions/profile/install.html similarity index 100% rename from www/extensions/profile/install.html rename to src/extensions/profile/install.html diff --git a/www/extensions/profile/static/index.css b/src/extensions/profile/static/index.css similarity index 100% rename from www/extensions/profile/static/index.css rename to src/extensions/profile/static/index.css diff --git a/src/extensions/profile/tables.ts b/src/extensions/profile/tables.ts new file mode 100644 index 0000000..c92609b --- /dev/null +++ b/src/extensions/profile/tables.ts @@ -0,0 +1,28 @@ +import { MigrationMap, Tables, VersionMap } from '../../classes/tables.ts' +import { Knex } from '../../modules.ts' + +export default class extends Tables { + override versions(versions: VersionMap) { + versions.set('device', 0) + + return versions + } + + override migrations(knex: Knex, migrations: MigrationMap) { + migrations.set('device', { + 0: async ()=>{ + await knex.schema().createTable('_device', (table) => { + table.increments('id').primary() + table.integer('user_id').notNullable() + table.foreign('user_id', 'fk_user_id').references('_root_user.id') + table.string('name') + table.uuid('uuid').notNullable() + table.string('ip').notNullable() + table.boolean('installed').notNullable().defaultTo(false).checkIn(['0','1']) + }) + } + }) + + return migrations + } +} diff --git a/www/extensions/profile/wireguard.js b/src/extensions/profile/wireguard.js similarity index 94% rename from www/extensions/profile/wireguard.js rename to src/extensions/profile/wireguard.js index 2d03b6e..8b1ae51 100644 --- a/www/extensions/profile/wireguard.js +++ b/src/extensions/profile/wireguard.js @@ -1,6 +1,6 @@ -const { exec } = require('child_process') -const fs = require('fs') -const readline = require('readline') +import { exec } from 'child_process' +import fs from 'fs' +import readline from 'readline' var tmp_dir = "" var configs_dir = "" @@ -8,7 +8,7 @@ var file_lines = [] var configs = {} var config = {} -exports.init = function (path, wg_config, tmp_path) { +export function init(path, wg_config, tmp_path) { config = wg_config configs_dir = path tmp_dir = tmp_path @@ -104,10 +104,10 @@ function __save_config() { * @param {Function} callback the callback to call when done */ function __remove(uuid, callback) { - loc = configs[uuid] + let loc = configs[uuid] if (!loc) return callback() // remove from server config - amount = loc.end - loc.start +1 + let amount = loc.end - loc.start +1 file_lines.splice(loc.start, amount) __save_config() // delete config file if still exists @@ -153,7 +153,7 @@ function __add(uuid, pubkey, prekey, privkey, ip, callback) { }) } -exports.create = function (uuid, ip, callback) { +export function create(uuid, ip, callback) { ip = config.subnet + ip.toString(16) exec("wg genkey", function (err, priv, stderr) { @@ -173,13 +173,13 @@ exports.create = function (uuid, ip, callback) { }) } -exports.delete = function (uuid, callback) { +export function remove(uuid, callback) { __remove(uuid, function () { return callback() }) } -exports.getConfig = function (uuid, callback) { +export function getConfig(uuid, callback) { fs.readFile(`${configs_dir+uuid}.conf`, 'utf8', function (err, data) { if (err) return callback(undefined, err) fs.unlink(`${configs_dir+uuid}.conf`, function (err) { diff --git a/www/extensions/root/content/getting-started.html b/src/extensions/root/content/getting-started.html similarity index 100% rename from www/extensions/root/content/getting-started.html rename to src/extensions/root/content/getting-started.html diff --git a/www/extensions/root/index.html b/src/extensions/root/index.html similarity index 100% rename from www/extensions/root/index.html rename to src/extensions/root/index.html diff --git a/src/extensions/root/index.ts b/src/extensions/root/index.ts new file mode 100644 index 0000000..ee737e1 --- /dev/null +++ b/src/extensions/root/index.ts @@ -0,0 +1,246 @@ +import crypto from 'crypto' +import { ExtensionBase, Knex } from "../../modules.ts" +import { readdirSync } from "fs" +import { unpack } from '../../util.ts' + + +export default class extends ExtensionBase implements RootExtension { + override name = 'root' + override title = 'Home' + override tables = true + + favicons: string[] = [] + favicons_path: string + + ip_scope: string + salt: string + + + override init = (context: InitContext) => { + let modules = context.modules + let data_path = context.data_path + this.ip_scope = context.modules.wg_config.ip_scope + this.salt = context.modules.config.salt + + this.favicons_path = data_path+'favicons/' + try { + this.favicons.push(...readdirSync(this.favicons_path)) + } catch { + (new modules.Log as Log).err(`No favicons found in '${this.favicons_path}'`) + } + + return ExtensionBase.init(this, context) + } + + override requires_login: Extension['requires_login'] = (path) => { + if (path.at(0) == '_') { + return true + } + return false + } + + override handle: Extension['handle'] = async (ctx) => { + var location = ctx.path.shift() + let [knex]: [Knex] = this.get_dependencies('Knex') + + switch (location) { + case '': + case undefined: { + if (!ctx.context.user) { + return this.return_text(ctx, 'index') + } + return this.return_html(ctx, 'user') + } + case 'login': { + // If user not logged in + if (!ctx.context.user) { + // Attempt + if (ctx.data) { + let form = ctx.data.form + // Login + if (form.login) { + let auth = ''; + if (form.username && form.password) { + auth = Buffer.from(form.username+":"+form.password).toString('base64') + } + return this.return_html(ctx, 'login', undefined, 500, 303, { + "Location": "/login", + "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) + }) + } + // Register + else if (form.register) { + this.addUser(form.username, form.password, (err?: Error) => { + // if invalid credentials + if (err) { + ctx.context.auth_err = err + return this.return_html(ctx, 'login') + } + // success + else { + let auth = Buffer.from(form.username+":"+form.password).toString('base64') + return this.return_html(ctx, 'login', undefined, 500, 303, { + "Location": "/", + "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) + }) + } + }) + return + } + } + // First load + return this.return_html(ctx, 'login', undefined, 500, 200, { + "Set-Cookie": this.del_cookie('auth') + }) + } + // if logged in + ctx.res.writeHead(307, {"Location": "/"}) + ctx.res.end() + return + } + case 'logout': { + if (ctx.context.user) { + // log user out and redirect + ctx.res.writeHead(307, { + "Location": "/", + "Set-Cookie": this.del_cookie('auth') + }) + ctx.res.end() + return + } + // if user is logged out + ctx.res.writeHead(307, {"Location": "/"}) + ctx.res.end() + return + } + case '_': { + if (ctx.context.user) { + var item = ctx.path.shift() + switch (item) { + case 'pfp': { + var args = ctx.req.url.split('?').at(1) + if (args) { + try { + args = decodeURIComponent(args) + } catch {} + + const head: [number, {}] = await knex.query('user') + .update('pfp_code', args) + .where('id', ctx.context.user.id) + .then( + () => [307, {"Location": "/"}], + () => [500, {}] + ) + + ctx.res.writeHead(...head) + ctx.res.end() + return + } + else + return this.return_html(ctx, 'pfp') + } + } + } + break + } + default: { + // Templated html + if (location.startsWith('~')) + return this.return_html(ctx, 'content/'+location.split('~')[1], undefined, 404) + // Favicon + else if (this.favicons.includes(location)) + return this.return_file(ctx, this.favicons_path+location) + // File + else + return this.return_file(ctx, location) + } + } + } + + authenticate: RootExtension['authenticate'] = async (auth, ip, subnet) => { + let [knex]: [Knex] = this.get_dependencies('Knex') + + if (auth) { + // Try to get name and password + const val = this.decrypt_auth(auth) + if (val instanceof Error) + return val + + const [name, password] = val + + // Auth using name and password + const [user, err] = await knex + .query('user') + .select('*') + .where('name', name) + .first() + .then(unpack) + + if (user && password == user.password) + return user + else + return new Error('Wrong name or password') + } + else if (ip.startsWith(subnet)) { + // Try using IP-address if no name and password + const user = await knex + .query({u: 'user', p: '_profile_device'}) + .select('u.*') + .join('_profile_device', 'u.id', '=', 'p.user_id') + .where('p.ip', ip) + .first() + + return user + } + } + + private decrypt_auth(auth: BasicAuth): [name: string, password: string] | Error { + // decode authentication string + let data = Buffer.from(auth.slice(6), 'base64').toString('utf-8') + + // get name and password + let [name, password] = data.split(":", 2) + if (!name || !password) { + return new Error("Missing name or password") + } + + // hash password + password = this.hash_pw(password) + + return [name, password] + } + + private hash_pw(password: string): string { + return crypto.pbkdf2Sync(password, this.salt, 10000, 128, 'sha512').toString('base64') + } + + addUser(name: User['name'], password: User['password'], callback: (err?: Error) => void) { + let [knex]: [Knex] = this.get_dependencies('Knex') + + password = this.hash_pw(password) + // Check if username is already taken + this.exists(name, (exists, err) => { + if (err) return callback(err) + if (exists) return callback(new Error("Username already taken")) + // add user to db + knex.query('user') + // @ts-expect-error + .insert({name, password, pfp_code: `seed=${name}`}) + .then(() => callback(), (err) => callback(err)) + }) + } + + private exists(name: User['name'], callback: (exists: boolean, err?: Error) => void): void { + let [knex]: [Knex] = this.get_dependencies('Knex') + + // check if name already exists + knex.query('user') + .select('id') + .where('name', name) + .then((value) => { + callback(!!value.length) + }, (err) => { + callback(false, err) + }) + } +} diff --git a/www/extensions/root/login.html b/src/extensions/root/login.html similarity index 100% rename from www/extensions/root/login.html rename to src/extensions/root/login.html diff --git a/www/extensions/root/pfp.html b/src/extensions/root/pfp.html similarity index 100% rename from www/extensions/root/pfp.html rename to src/extensions/root/pfp.html diff --git a/www/extensions/root/static/favicon.ico b/src/extensions/root/static/favicon.ico similarity index 100% rename from www/extensions/root/static/favicon.ico rename to src/extensions/root/static/favicon.ico diff --git a/www/extensions/root/static/index.css b/src/extensions/root/static/index.css similarity index 100% rename from www/extensions/root/static/index.css rename to src/extensions/root/static/index.css diff --git a/www/extensions/root/static/keuknet.png b/src/extensions/root/static/keuknet.png similarity index 100% rename from www/extensions/root/static/keuknet.png rename to src/extensions/root/static/keuknet.png diff --git a/www/extensions/root/static/pfp.js b/src/extensions/root/static/pfp.js similarity index 100% rename from www/extensions/root/static/pfp.js rename to src/extensions/root/static/pfp.js diff --git a/www/extensions/root/static/wallpaper.jpg b/src/extensions/root/static/wallpaper.jpg similarity index 100% rename from www/extensions/root/static/wallpaper.jpg rename to src/extensions/root/static/wallpaper.jpg diff --git a/src/extensions/root/tables.ts b/src/extensions/root/tables.ts new file mode 100644 index 0000000..3430619 --- /dev/null +++ b/src/extensions/root/tables.ts @@ -0,0 +1,33 @@ +import { MigrationMap, Tables, VersionMap } from '../../classes/tables.ts' +import { Knex } from '../../modules.ts' + +export default class extends Tables { + override versions(versions: VersionMap) { + versions.set('user', 1) + + return versions + } + + override migrations(knex: Knex, migrations: MigrationMap) { + migrations.set('user', { + 0: async ()=>{ + await knex.schema() + .createTable('user', (table) => { + table.increments('id').primary() + table.string('name').notNullable().unique() + table.string('password').notNullable() + table.timestamp('registration_date').notNullable().defaultTo(knex.raw('CURRENT_TIMESTAMP')) + table.boolean('is_admin').notNullable().defaultTo(false).checkIn(['0','1']) + }) + }, + 1: async ()=>{ + await knex.schema() + .alterTable('user', (table) => { + table.string('pfp_code') + }) + }, + }) + + return migrations + } +} diff --git a/www/extensions/root/user.html b/src/extensions/root/user.html similarity index 100% rename from www/extensions/root/user.html rename to src/extensions/root/user.html diff --git a/src/extman.ts b/src/extman.ts new file mode 100644 index 0000000..e337e46 --- /dev/null +++ b/src/extman.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex" + +export async function load(modules: any, namespace: string, knex: Knex): Promise { + let context: InitContext = { + modules, + path: `${import.meta.dirname}/extensions/${namespace}/`, + data_path: `${import.meta.dirname}/../data/${namespace}/`, + name: namespace, + knex, + } + + let ext = new (await import(`./extensions/${namespace}/index`)).default as Extension + let status = ext.init(context) + + if (status instanceof Promise) + status.catch(err => console.error(`Failed initializing [${namespace}]: ${err}`)) + + return ext +} diff --git a/src/handle.ts b/src/handle.ts new file mode 100644 index 0000000..952f9af --- /dev/null +++ b/src/handle.ts @@ -0,0 +1,86 @@ +import { load } from "./extman.ts" +import Log from "./modules/log.ts" +import { unpack } from "./util.ts" + +let log = new Log(true) + +export default class implements Handle { + extensions_list = [ + 'profile','nothing','admin','chat' + ] + root: RootExtension + wg_config: any + extensions = new Map() + admin_extensions = new Map() + + constructor(modules: any) { + let config = modules.config + this.wg_config = modules.wg_config + let nj: Environment = modules.nj + nj.addGlobal('dicebear_host', config.dicebear_host) + nj.addGlobal('client_location', config.client_location) + } + + init: Handle['init'] = async (modules, knex) => { + this.root = await load(modules, 'root', knex) as RootExtension + + for (const path of this.extensions_list) { + try { + this.extensions.set(path, await load(modules, path, knex) as Extension) + } catch (err: any) { + log.err(`Unable to load extension '${path}':\n\t${err.message}\n${err.stack}`) + } + } + + this.extensions.forEach((extension, name, m) => { + if (extension.admin_only) { + this.admin_extensions.set(name, extension) + this.extensions.delete(name) + } + }) + } + + main: Handle['main'] = async (partial_ctx: PartialContext) => { + let location = partial_ctx.path.shift() ?? '' + + // set request context + let ctx: Context = { + ...partial_ctx, + context: { + ...partial_ctx.args, + extensions: this.extensions, + location, + } + } + + // Authenticate using user&pass, else using ip + const [user, err] = await this.root.authenticate(ctx.req.headers.authorization as BasicAuth|undefined, ctx.ip, this.wg_config.subnet).then(unpack) + + ctx.context.user = user + ctx.context.auth_err = err + + if (user && user.is_admin) + ctx.context.extensions = {...ctx.context.extensions, ...this.admin_extensions} + + // Extension + const selected_extension = ctx.context.extensions.get(location) + if (selected_extension) { + // If login required + if (!user && selected_extension.requires_login(ctx.path)) { + ctx.res.writeHead(307, {Location: "/login"}) + ctx.res.end() + } + else if (user && !user.is_admin && selected_extension.requires_admin(ctx.path)) { + ctx.res.writeHead(307, {Location: "/"}) + ctx.res.end() + } + else + selected_extension.handle_req(ctx) + } + // Root extension + else { + ctx.path.unshift(location) + this.root.handle_req(ctx) + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..867101d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,211 @@ +import http2 from 'http2' +import http1, { IncomingMessage, ServerResponse } from 'http' +import Knex from 'knex' +import dotenv from 'dotenv' +import {cookie, config, Log } from './modules.ts' +import * as modules from './modules.ts' +import Handle from './handle.ts' + +// enable use of dotenv +dotenv.config() + +// init database +const knex = Knex({ + client: 'sqlite3', + connection: { + filename: `${import.meta.dirname}/../data/db.sqlite` + }, + /** + * `__` => selects `__
` + * `_
` => selects `__
` + * `
` => selects `
` + */ + wrapIdentifier(value, origImpl, queryContext) { + if (queryContext !== undefined && 'prefix' in queryContext) { + if (value.startsWith('_')) { + if (!value.substring(1).includes('_')) { + value = `_${queryContext.prefix}${value}` + } + } + } + return origImpl(value) + }, +}) + +// prepare database +if (!await knex.schema.hasTable('db_table_versions')) { + await knex.schema + .createTable('db_table_versions', (table) => { + table.string('table_id').notNullable().unique() + table.integer('version').notNullable().defaultTo(1) + }) +} +// get request handler +const handle = new Handle(modules) +handle.init(modules, knex) + +// set up logging +const log = new Log(config.logging) + +// handle all requests for both HTTPS and HTTP/2 or HTTP/nginx +async function requestListener(req: Http2ServerRequest, res: Http2ServerResponse) { + let ip: Context['ip'] + let cookies: Context['cookies'] + let args: Context['args'] = new Map() + let path: Context['path'] + let data: Context['data'] + + + if (process.env.DEV) { + req.headers.host = config.domain + ip = process.env.IP || '0.0.0.0' + } + + cookies = cookie.parse(req.headers.cookie || '') + // get authorization info + req.headers.authorization ??= cookies.auth + // get requested host, HTTP/<=1.1 uses host, HTTP/>=2 uses :authority + req.headers.host ??= req.headers[':authority'] + + // If standalone + if (!config.nginx) { + // get requesting IP + ip ??= req.socket?.remoteAddress || req.connection?.remoteAddress || '0.0.0.0' + + // if request is not for any domain served here, act like server isn't here + if (req.headers.host != config.domain) { + log.con_err(req) + return + } + // If behind NGINX + } else { + // get requesting IP + ip = req.headers['x-real-ip'] as string || '0.0.0.0' + } + + { + // separate url arguments from the url itself + let [raw_path = req.url, raw_args = ''] = req.url.split('?', 2) + + // split arguments into key:value pairs + if (raw_args != '') { + for (let arg of raw_args.split('&')) { + let [key, value] = arg.split('=', 2) + if (!(key === undefined || value === undefined)) + args.set(key, value) + } + } + + // split url into path items + path = raw_path.split('/').slice(1) + } + + // wait for all data if posting/putting + if (req.method == 'POST' || req.method == 'PUT') { + data = await new Promise((resolve) => { + let buffer: Buffer[] = [] + req.on('data', function(data: Buffer) { + buffer.push(data) + }) + req.on('end', function() { + let bytes = Buffer.concat(buffer as readonly Uint8Array[]) + data = { + bytes, + raw: bytes.toString(), + form: {} + } + data.raw.split('&').forEach((i: string) => { + let [k,v] = i.split('=') + if (!!k && !!v) { + // @ts-ignore + data.form[k] = decodeURIComponent(v).replace(/\+/g,' ') + } + }) + resolve(data) + }) + }) + } + + let ctx: PartialContext = { + req, + res, + path, + args, + cookies, + ip, + data, + } + // log the request + log.con(req, ctx) + // forward the request to the handler + handle.main(ctx) +} + +function requestListenerCompat(req: IncomingMessage, res: ServerResponse) { + const new_req = { + authority: req.headers.host ?? '', + scheme: new URL(req.url ?? '').protocol, + ...req, + } as unknown as Http2ServerRequest + + const new_res = { + ...res, + } as unknown as Http2ServerResponse + + return requestListener(new_req, new_res) +} + +// Redirect requests to HTTPS +function httpsRedirect(req: IncomingMessage, res: ServerResponse) { + res.writeHead(307, {"Location": `https://${req.headers.host}${req.url}`}) + res.end() +} + + +async function startServer(http_enabled: boolean, https_enabled: boolean) { + if (https_enabled) { + let key = async (location: string) => { + return new Promise(async (resolve, reject) => { + (await import('fs')).promises.readFile(location, "utf8") + .then(resolve) + .catch((err: Error) => { + log.err(`Failed fetching key at: '${location}'`) + resolve(undefined) + }) + }) + } + + log.status("Fetching encryption keys") + + const private_key = await key(config.private_key_path) + const certificate = await key(config.server_cert_path) + const certificate_authority = await key(config.ca_cert_path) + + log.status("Encryption keys fetched") + + // Start server + http2.createSecureServer({ + key: private_key, + cert: certificate, + ca: certificate_authority, + allowHTTP1: true, + }, requestListener) + .listen( + config.https_port, + config.host, + () => log.serverStart("https", config.domain, config.host, config.https_port) + ) + } + if (http_enabled) { + // Start server + http1.createServer( + https_enabled ? httpsRedirect : requestListenerCompat + ).listen( + config.http_port, + config.host, + () => log.serverStart("http", config.domain, config.host, config.http_port) + ) + } +} + +startServer(true, !config.nginx) diff --git a/src/modules.ts b/src/modules.ts new file mode 100644 index 0000000..c417952 --- /dev/null +++ b/src/modules.ts @@ -0,0 +1,16 @@ +import nunjucks from 'nunjucks' +export const nj: Environment = nunjucks.configure([ + `${import.meta.dirname}/templates`, + `${import.meta.dirname}/extensions` +]) + +export * as cookie from 'cookie' +export {ExtensionBase} from './classes/extension.ts' +export {Tables} from './classes/tables.ts' +export {default as Knex} from './modules/knex.ts' +export {default as Log} from './modules/log.ts' +export {default as content} from './modules/content_type.ts' +export {default as Fetch} from './modules/fetch.ts' +export {default as config} from '../config/config.ts' +export {default as wg_config} from '../config/wireguard.ts' +export {default as texts} from '../config/texts.ts' diff --git a/src/modules/content_type.ts b/src/modules/content_type.ts new file mode 100644 index 0000000..1299b52 --- /dev/null +++ b/src/modules/content_type.ts @@ -0,0 +1,29 @@ +// Source: https://stackoverflow.com/a/51398471/15181929 +export default (class implements Module { + static readonly HTML = {"Content-Type": "text/html"} + static readonly ASCII = {"Content-Type": "text/plain charset us-ascii"} + static readonly TXT = {"Content-Type": "text/plain charset utf-8"} + static readonly JSON = {"Content-Type": "application/json"} + static readonly ICO = {"Content-Type": "image/x-icon", "Cache-Control": "private, max-age=3600"} + static readonly CSS = {"Content-Type": "text/css", "Cache-Control": "private, max-age=3600"} + static readonly GIF = {"Content-Type": "image/gif", "Cache-Control": "private, max-age=3600"} + static readonly JPG = {"Content-Type": "image/jpeg", "Cache-Control": "private, max-age=3600"} + static readonly JS = {"Content-Type": "text/javascript", "Cache-Control": "private, max-age=3600"} + static readonly PNG = {"Content-Type": "image/png", "Cache-Control": "private, max-age=3600"} + static readonly MD = {"Content-Type": "text/x-markdown"} + static readonly XML = {"Content-Type": "application/xml"} + static readonly SVG = {"Content-Type": "image/svg+xml", "Cache-Control": "private, max-age=3600"} + static readonly WEBMANIFEST = {"Content-Type": "application/manifest+json", "Cache-Control": "private, max-age=3600"} + static readonly MP3 = {"Content-Type": "audio/mpeg", "Cache-Control": "private, max-age=3600"} + static readonly EXE = {"Content-Type": "application/vnd.microsoft.portable-executable", "Cache-Control": "private, max-age=3600"} + static readonly PY = {"Content-Type": "text/x-python", "Cache-Control": "private, max-age=3600"} + + // Force singleton + private constructor(private readonly key: string, public readonly value: any) { + } + + init: Module['init'] = (_context) => { + return [true] + } + +}) as ContentType diff --git a/src/modules/fetch.ts b/src/modules/fetch.ts new file mode 100644 index 0000000..4d014dc --- /dev/null +++ b/src/modules/fetch.ts @@ -0,0 +1,46 @@ +import { readFile } from "fs/promises" +import * as path from "path" +import { unpack } from "../util.ts" + +export default class implements Module, Fetch { + /** File extensions of binary filetypes */ + private readonly binary_file_name_extensions: Set = new Set(['png','jpg','mp3']) + /** Caches processed files */ + private cache: Map = new Map() + private root: string + + + init: Module['init'] = (context) => { + this.root = path.join(context.path, "/static/") + return [true] + } + + file: Fetch['file'] = async (file_path) => { + // Ensure path is absolute + if (!path.isAbsolute(file_path)) + file_path = path.join(this.root, file_path) + + file_path = path.normalize(file_path) + let filetype = path.extname(file_path).substring(1) + + // load from cache if available + if (this.cache.has(file_path)) + return [this.cache.get(file_path) as string, filetype] + + // read the file + const [raw_data, err] = await readFile(file_path).then(unpack, unpack) + if (err) + return err + + // cache and return the data + let data: FileData = (()=>{ + if (this.binary_file_name_extensions.has(filetype)) + return raw_data as unknown as string + else + return raw_data.toString('utf8') + })() + + this.cache.set(file_path, data) + return [data, filetype] + } +} diff --git a/src/modules/knex.ts b/src/modules/knex.ts new file mode 100644 index 0000000..0bc9519 --- /dev/null +++ b/src/modules/knex.ts @@ -0,0 +1,24 @@ +import { Knex } from "knex" + +export default class implements Module { + private knex: Knex + private prefix: string + + init: Module['init'] = (context: InitContext) => { + this.knex = context.knex + this.prefix = context.name + return [true] + } + + raw = (value: any) => { + return this.knex.raw(value) + } + + query = (table: string | object) => { + return this.knex(table as never).queryContext({prefix: this.prefix}) + } + + schema = () => { + return this.knex.schema.queryContext({prefix: this.prefix}) + } +} diff --git a/src/modules/log.ts b/src/modules/log.ts new file mode 100644 index 0000000..4dce31e --- /dev/null +++ b/src/modules/log.ts @@ -0,0 +1,66 @@ +export default class implements Log { + we_logging = false; + + constructor(we_log: boolean) { + this.we_logging = we_log + } + + private mask_ip(ip: string) { + let tmp = "" + // if IPv4 + if (ip.includes('.')) { + // strip 4to6 prefix + ip = ip.substring(ip.lastIndexOf(':') + 1, ip.length) + // mask ip + ip.split('.').forEach(function (quad: string, index: number) { + quad = quad.padStart(3, "0") + if (index <= 2) tmp += quad + "." + if (index == 2) tmp += "*" + }) + } + else { + // mask ip + ip.split(':').forEach(function (quad: string, index: number) { + quad = quad.padStart(4, "0") + if (index <= 3) tmp += quad + ":" + if (index == 3) tmp += "*" + }) + } + return tmp + } + + private mask_url(url: string) { + return url.split('?')[0] + } + + con(req: Http2ServerRequest, ctx: Context | PartialContext) { + if (this.we_logging) { + let ip = this.mask_ip(ctx.ip || '') + let url = this.mask_url(req.url) + console.log( + `\x1b[32m [${ip}]=>'${req.method} ${url} + HTTP/${req.httpVersion} ${(req.headers['user-agent'] ?? "NULL").split(" ", 1)[0]} ${req.headers.authorization ? "auth" : "noauth"}\x1b[0m` + ) + } + } + + con_err(req: Http2ServerRequest) { + if (this.we_logging) { + console.log( + `\x1b[35m DENIED: '${req.headers.host}'\x1b[0m` + ) + } + } + + status(msg: any) { + console.log(`\x1b[34m>> ${msg}\x1b[0m`) + } + + err(err: any) { + console.log(`\x1b[31m>> ${err}\x1b[0m`) + } + + serverStart(type: string, domain: any, host: any, port: any) { + console.log(`\x1b[1m${type.toUpperCase()} server running on ${type}://${domain}:${port}, interface '${host}'\n\x1b[0m`) + } +} diff --git a/www/templates/extension.html b/src/templates/extension.html similarity index 100% rename from www/templates/extension.html rename to src/templates/extension.html diff --git a/www/templates/includes/description.html b/src/templates/includes/description.html similarity index 100% rename from www/templates/includes/description.html rename to src/templates/includes/description.html diff --git a/www/templates/includes/extensions.html b/src/templates/includes/extensions.html similarity index 100% rename from www/templates/includes/extensions.html rename to src/templates/includes/extensions.html diff --git a/www/templates/includes/favicons.html b/src/templates/includes/favicons.html similarity index 100% rename from www/templates/includes/favicons.html rename to src/templates/includes/favicons.html diff --git a/www/templates/includes/header.html b/src/templates/includes/header.html similarity index 100% rename from www/templates/includes/header.html rename to src/templates/includes/header.html diff --git a/www/templates/layout.html b/src/templates/layout.html similarity index 100% rename from www/templates/layout.html rename to src/templates/layout.html diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..3ffa7c4 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,4 @@ +export function unpack(val: S): [S, undefined] | [undefined, Error] { + if (val instanceof Error) return [undefined, val] + else return [val, undefined] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..726adc2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "outDir": "./build", + "allowJs": true, + "target": "ESNext", + "moduleResolution": "nodenext", + "module": "NodeNext", + "diagnostics": true, + // "alwaysStrict": true, + // "strict": true, + "noImplicitThis": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "strictNullChecks": true, + "typeRoots": ["./node_modules/@types", "./types/**/*"], + "allowImportingTsExtensions": true, + "noEmit": true, + }, + "ts-node": { + "files": true, + }, + "exclude": ["./build"], +} diff --git a/types/classes/extension.d.ts b/types/classes/extension.d.ts new file mode 100644 index 0000000..f96fd08 --- /dev/null +++ b/types/classes/extension.d.ts @@ -0,0 +1,46 @@ +declare interface Extension { + admin_only: boolean + tables: boolean + + name: string + title: string + + init(context: InitContext): void | Promise + + requires_login(path: string[]): boolean + + requires_admin(path: string[]): boolean + + handle_req(ctx: Context): Promise + + handle(ctx: Context): void | Error | Promise + + return(ctx: Context, err?: Error, location?: string, err_code?: number): void + + return_text(ctx: Context, item: string): void + + return_html(ctx: Context, item: string, err?: Error, err_code?: number, success_code?: number, headers?: any): void + + return_file(ctx: Context, file: string): void + + return_data(ctx: Context, data: any, err?: Error, headers?: {}, err_code?: number): void + + set_cookie(key: string, value: any, secure?: boolean): string + + del_cookie(key: string): string + + get_dependencies: DependencyMap['massGet'] +} + +declare interface RootExtension extends Extension { + authenticate(auth: BasicAuth|undefined, ip: string, subnet: string): Promise + addUser(name: User['name'], password: User['password'], callback: (err?: Error) => void): void +} + +declare interface DependencyMap { + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void + has(key: K): boolean + set(key: string, value: any): void + get(key: string): any + massGet(...items: T): { [K in keyof T]: any } +} \ No newline at end of file diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..ade90e8 --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,66 @@ +declare type Http2ServerRequest = import('http2').Http2ServerRequest +declare type Http2ServerResponse = import('http2').Http2ServerResponse +declare type Environment = import('nunjucks').Environment +declare type Database = import('sqlite3').Database + +declare type HttpHeader = { + "Content-Type"?: string, + "Cache-Control"?: string +} + +declare type BasicAuth = `Basic ${string}` + +declare type FileData = string | null + +declare type Context = { + req: Http2ServerRequest + res: Http2ServerResponse + + ip: string + cookies: Record + path: string[] + args: Map + data?: {bytes: Buffer, raw: string, form: any} + context: { + user?: User + extensions: Map + location: string + [any: string]: any + } +} + +declare type PartialContext = { + req: Http2ServerRequest + res: Http2ServerResponse + + ip: string + cookies: Record + path: string[] + args: Map + data?: {bytes: Buffer, raw: string, form: any} +} + +declare type User = { + id: number, + name: string, + password: string, + regdate: Date, + is_admin: boolean, + pfp_code: string +} + +declare type InitContext = { + modules: any, + path: string, + data_path: string, + name: string, + knex: import('knex').Knex, +} + +declare type ResultStatus = [Okay: false, Error: Error] | [Okay: true] + +declare type VariableSizeArray = { [K in keyof S]: T } + +declare interface Module { + init(context: InitContext): ResultStatus +} diff --git a/types/handle.d.ts b/types/handle.d.ts new file mode 100644 index 0000000..6980ca0 --- /dev/null +++ b/types/handle.d.ts @@ -0,0 +1,4 @@ +declare interface Handle { + init(modules: any, knex: import('knex').Knex): Promise + main(ctx: PartialContext): void, +} diff --git a/types/modules/content_type.d.ts b/types/modules/content_type.d.ts new file mode 100644 index 0000000..7781b49 --- /dev/null +++ b/types/modules/content_type.d.ts @@ -0,0 +1,19 @@ +declare interface ContentType { + readonly HTML: HttpHeader + readonly ASCII: HttpHeader + readonly TXT: HttpHeader + readonly JSON: HttpHeader + readonly ICO: HttpHeader + readonly CSS: HttpHeader + readonly GIF: HttpHeader + readonly JPG: HttpHeader + readonly JS: HttpHeader + readonly PNG: HttpHeader + readonly MD: HttpHeader + readonly XML : HttpHeader + readonly SVG: HttpHeader + readonly WEBMANIFEST: HttpHeader + readonly MP3: HttpHeader + readonly EXE: HttpHeader + readonly PY: HttpHeader +} diff --git a/types/modules/fetch.d.ts b/types/modules/fetch.d.ts new file mode 100644 index 0000000..d493efa --- /dev/null +++ b/types/modules/fetch.d.ts @@ -0,0 +1,3 @@ +declare interface Fetch { + file(file_path: string): Promise<[FileData, string] | Error> +} diff --git a/types/modules/log.d.ts b/types/modules/log.d.ts new file mode 100644 index 0000000..e0e75c9 --- /dev/null +++ b/types/modules/log.d.ts @@ -0,0 +1,7 @@ +declare interface Log { + con(req: any, ctx: any): void, + con_err(req: any): void, + status(msg: string): void, + err(err: string): void, + serverStart(type: string, domain: string, host: string, port: number): void +} diff --git a/www/extensions/admin/index.js b/www/extensions/admin/index.js deleted file mode 100644 index 34c32e6..0000000 --- a/www/extensions/admin/index.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = (Extension) => {return class extends Extension { - name = 'admin' - title = 'Admin' - admin_only = true - dependencies = ['data','content','nj'] - - handle(req, res) { - this.return_html(req, res, 'index') - } -}} diff --git a/www/extensions/chat/index.js b/www/extensions/chat/index.js deleted file mode 100644 index 4af2c90..0000000 --- a/www/extensions/chat/index.js +++ /dev/null @@ -1,53 +0,0 @@ -module.exports = (Extension) => {return class extends Extension { - name = 'chat' - title = 'Chat' - dependencies = ['content','nj','fetch'] - messages = [{ - user: {name:'SYSTEM',pfp_code:'seed=SYSTEM'}, - time:(new Date()).toLocaleTimeString('en-US', {hour12: false}), - content: 'Welcome to the chatroom!' - }] - last_got_id = {} - - requires_login(path) { - return true - } - - handle(req, res) { - var location = req.path.shift() - if (!location) { - if (req.data && req.data.message) { - var message = req.data.message.substring(0,255) - var now = (new Date()).toLocaleTimeString('en-US', {hour12: false}) - this.messages.push({ - user: {name:req.context.user.name, pfp_code:req.context.user.pfp_code}, - time: now, - content: message - }) - } - req.context.chat = this.messages - this.last_got_id[req.context.user.id] = this.messages.length - return this.return_html(req, res, 'index') - } - else if (location == 'getnew') { - var part = this.last_got_id.hasOwnProperty(req.context.user.id) ? this.last_got_id[req.context.user.id] : 0 - this.last_got_id[req.context.user.id] = this.messages.length - return this.return_data(res, `{"messages":${JSON.stringify(this.messages.slice(part))}}`) - } - else if (location == 'postmessage') { - if (req.data && req.data.message) { - var message = req.data.message.substring(0,255) - var now = (new Date()).toLocaleTimeString('en-US', {hour12: false}) - this.messages.push({ - user: {name:req.context.user.name, pfp_code:req.context.user.pfp_code}, - time: now, - content: message - }) - } - return this.return(res) - } - else { - return this.return_file(res, location) - } - } -}} diff --git a/www/extensions/nothing/index.js b/www/extensions/nothing/index.js deleted file mode 100644 index 304fd05..0000000 --- a/www/extensions/nothing/index.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = (Extension) => {return class extends Extension { - name = 'nothing' - title = 'Nothing' - dependencies = ['content','nj'] - - handle(req, res) { - this.return_html(req, res, 'index') - } -}} diff --git a/www/extensions/profile/index.js b/www/extensions/profile/index.js deleted file mode 100644 index 8c9ad5c..0000000 --- a/www/extensions/profile/index.js +++ /dev/null @@ -1,114 +0,0 @@ -module.exports = (Extension) => {return class extends Extension { - name = 'profile' - title = 'Network' - tables = true - dependencies = ['content','nj','fetch'] - crypto = require('crypto') - wg = require('./wireguard') - wg_config = null - - constructor (global, path, data_path) { - super(global, path) - this.wg.init(data_path, global.wg_config, global.config.tmp_dir) - this.wg_config = global.wg_config - } - - - requires_login(path) { - if (path.at(0) == 'getconf') { - return false - } - return true - } - - handle(req, res) { - var location = req.path.shift() - switch (location) { - case '': - case undefined: { - this.db.select('device', ['*'], 'user_id=$id', null, [req.context.user.id], (err, profiles) => { - req.context.profiles = profiles - req.context.connected_ip = req.ip.startsWith(this.wg_config.subnet) ? req.ip : false - this.return_html(req, res, 'index', err) - }) - break - } - case 'delete': { - // Check ownership - this.owns(req.context.user, req.args.uuid, (user_owns) => { - if (!user_owns) return this.return(res, true, 404) - // Delete db entry - this.db.delete('device', 'uuid=$uuid', [req.args.uuid], (err) => { - // Delete wireguard profile - this.wg.delete(req.args.uuid, () => { - return this.return(res, err, location='/profile') - }) - }) - }) - break - } - case 'add': { - // Get uuid - let uuid = this.crypto.randomUUID() - // Get IP suffix - this.db.select('device', ['MAX(id)'], null, null, [], (err, data) => { - let id = data[0]['MAX(id)'] +2 - // Register wireguard link - this.wg.create(uuid, id, (ip, err) => { - if (err) return callback(err) - // Insert in db - this.db.insert('device', ['user_id','uuid','ip'], [req.context.user.id, uuid, ip], (err) => { - return this.return(res, err, location='/profile') - }) - }) - }) - break - } - case 'getconf': { - // Get uuid - let uuid = Object.keys(req.args)[0] - // Get config - this.wg.getConfig(uuid, (data, err) => { - if (err) return this.return(res, true, 404) - // Mark as installed - this.db.update('device', ['installed=TRUE'], 'uuid=$uuid', [uuid], (err) => { - return this.return_data(res, data, err, {"Content-Type": "text/plain charset utf-8", "Content-Disposition": `attachment; filename="keuknet.conf"`}) - }) - }) - break - } - case 'install': { - req.context.host = req.headers.host - return this.return_html(req, res, 'install') - break - } - case 'rename': { - if (req.data) { - // Check ownership - this.owns(req.context.user, req.args.uuid, (user_owns) => { - if (!user_owns) return this.return(res, true, 404) - // Change name - this.db.update('device', ['name=$name'], 'uuid=$uuid', [req.post_data,req.args.uuid], (err) => { - return this.return(res, err, location='/profile') - }) - }) - } - else { - req.context = {item:"new name",action:req.url,destination:"/profile"} - this.return_html(req, res, 'edit') - } - break - } - default: { - return this.return_file(res, location) - } - } - } - - owns = (user, uuid, callback) => { - if (!uuid) return callback(undefined) - this.db.select('device', ['1'], 'user_id=$id AND uuid=$uuid', null, [user.id, uuid], (err, data) => { - return callback(data[0] ? Object.hasOwn(data[0], '1') : false) - }) - } -}} diff --git a/www/extensions/profile/tables.js b/www/extensions/profile/tables.js deleted file mode 100644 index be8cb98..0000000 --- a/www/extensions/profile/tables.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = (Tables) => {return class extends Tables { - tables = { - 'device': 0 - } - - device = { - 0:()=>{ - this.db.createTable('device', [ - 'id INTEGER PRIMARY KEY AUTOINCREMENT', - 'user_id INTEGER NOT NULL', - 'name VARCHAR', - 'uuid CHAR(36) NOT NULL', - 'ip VARCHAR NOT NULL', - 'installed BOOLEAN NOT NULL DEFAULT FALSE CHECK (installed IN (0,1))', - 'CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES user(id)' - ]) - } - } -}} diff --git a/www/extensions/root/index.js b/www/extensions/root/index.js deleted file mode 100644 index 77573d6..0000000 --- a/www/extensions/root/index.js +++ /dev/null @@ -1,134 +0,0 @@ -module.exports = (Extension) => {return class extends Extension { - name = 'root' - title = 'Home' - tables = true - dependencies = ['content','nj','fetch','data','texts','cookie'] - favicons = [] - favicons_path - - constructor (global, path, data_path) { - super(global, path) - - this.favicons_path = data_path+'favicons/' - let fs = require('fs') - try { - this.favicons.push(...fs.readdirSync(this.favicons_path)) - } catch { - global.log.err(new Error(`No favicons found in '${this.favicons_path}'`)) - } - } - - - requires_login(path) { - if (path.at(0) == '_') { - return true - } - return false - } - - handle(req, res) { - var location = req.path.shift() - switch (location) { - case '': - case undefined: { - if (!req.context.user) { - return this.return_text(req, res, 'index') - } - return this.return_html(req, res, 'user') - } - case 'login': { - // If user not logged in - if (!req.context.user) { - // Attempt - if (req.data) { - // Login - if (req.data.login) { - let auth = ''; - if (req.data.username && req.data.password) { - auth = Buffer.from(req.data.username+":"+req.data.password).toString('base64') - } - return this.return_html(req, res, 'login', null, 500, 303, { - "Location": "/login", - "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) - }) - } - // Register - else if (req.data.register) { - return this.data.addUser(req.data.username, req.data.password, (err) => { - // if invalid credentials - if (err) { - req.context.auth_err = err - return this.return_html(req, res, 'login', null) - } - // success - else { - let auth = Buffer.from(req.data.username+":"+req.data.password).toString('base64') - return this.return_html(req, res, 'login', null, 500, 303, { - "Location": "/", - "Set-Cookie": this.set_cookie('auth', 'Basic '+auth, true) - }) - } - }) - } - } - // First load - return this.return_html(req, res, 'login', null, 500, 200, { - "Set-Cookie": this.del_cookie('auth') - }) - } - // if logged in - res.writeHead(307, {"Location": "/"}) - return res.end() - } - case 'logout': { - if (req.context.user) { - // log user out and redirect - res.writeHead(307, { - "Location": "/", - "Set-Cookie": this.del_cookie('auth') - }) - return res.end() - } - // if user is logged out - res.writeHead(307, {"Location": "/"}) - return res.end() - } - case '_': { - var item = req.path.shift() - switch (item) { - case 'pfp': { - var args = req.url.split('?').at(1) - if (args) { - try { - args = decodeURIComponent(args) - } catch {} - this.db.update('user', ['pfp_code=$args'], 'id=$id', [args, req.context.user.id], (err) => { - res.writeHead(307, {"Location": "/"}) - res.end() - }) - return - } - else { - return this.return_html(req, res, 'pfp', null) - } - } - } - break - } - default: { - // Templated html - if (location.startsWith('~')) { - return this.return_html(req, res, 'content/'+location.split('~')[1], null, 404) - } - // Favicon - else if (this.favicons.includes(location)) { - return this.return_file(res, this.favicons_path+location) - } - // File - else { - return this.return_file(res, location) - } - } - } - } -}} diff --git a/www/extensions/root/tables.js b/www/extensions/root/tables.js deleted file mode 100644 index 5613cb7..0000000 --- a/www/extensions/root/tables.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = (Tables) => {return class extends Tables { - tables = { - 'user': 1, - 'db_table_versions': 0 - } - - user = { - 0:()=>{ - this.db.createTable('user', [ - 'id INTEGER PRIMARY KEY AUTOINCREMENT', - 'name VARCHAR NOT NULL', - 'password VARCHAR NOT NULL', - 'regdate TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL', - 'is_admin BOOLEAN NOT NULL DEFAULT FALSE CHECK (is_admin IN (0,1))', - ]) - }, - 1:()=>{ - this.db.select('user', ['id','name'], null, null, [], (err, data) => { - if(err)console.log(err) - this.db.addColumn('user', 'pfp_code TEXT', (err) => { - if(err)console.log(err) - for (const user of data) { - this.db.update('user', ['pfp_code=$name'], 'id=$id', [`seed=${user.name}`, user.id], (err)=>{if(err)console.log(err)}) - } - }) - }) - } - } - db_table_versions = { - 0:()=>{ - this.db.createTable('db_table_versions', [ - 'id INTEGER PRIMARY KEY AUTOINCREMENT', - 'table_id VARCHAR NOT NULL', - 'version INTEGER NOT NULL DEFAULT 1' - ]) - } - } -}} diff --git a/www/extensions/servers/addserver.html b/www/extensions/servers/addserver.html deleted file mode 100644 index 4e004b2..0000000 --- a/www/extensions/servers/addserver.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "extension.html" %} - -{% block body %} - -

Add new server

-
- - - - - - - - - - - - - -{% endblock %} diff --git a/www/extensions/servers/index.js b/www/extensions/servers/index.js deleted file mode 100644 index 706a2b6..0000000 --- a/www/extensions/servers/index.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = (Extension) => {return class extends Extension { - name = 'servers' - title = 'Servers' - tables = true - dependencies = ['content','nj','fetch'] - - requires_admin(path) { - if (path.at(0) == "addserver") { - return true - } - return false - } - - handle(req, res) { - var location = req.path.shift() - - if (!location) { - if (req.args.server) { - this.db.select('server', ['*'], 'id=$id', null, [req.args.server], (err, server) => { - req.context.server = server[0] - this.return_html(req, res, 'server', err ?? !server[0], 404) - }) - return - } - this.db.select('server', ['*'], null, null, [], (err, servers) => { - req.context.servers = servers - this.return_html(req, res, 'serverlist', err) - }) - return - } - if (location == "addserver") { - if (req.data) { - if (req.data.name && req.data.admin_id && req.data.description && req.data.ip) { - this.db.insert('server', ['name','admin_id','description','ip','url'], [req.data.name,req.data.admin_id,req.data.description,req.data.ip,req.data.url], (err) => { - this.return(res, err) - }) - return - } - } - return this.return_html(req, res, 'addserver') - } - else { - return this.return_file(res, location) - } - } -}} diff --git a/www/extensions/servers/server.html b/www/extensions/servers/server.html deleted file mode 100644 index fda25bb..0000000 --- a/www/extensions/servers/server.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "extension.html" %} - -{% block head %} - -{% endblock %} - -{% block body %} -
- {% if server.url %} -

{{server.name}}

- {% else %} -

{{server.name}}

- {% endif %} -

IP: {{server.ip}}

-

{{server.description | safe}}

-
-{% endblock %} diff --git a/www/extensions/servers/serverlist.html b/www/extensions/servers/serverlist.html deleted file mode 100644 index 2a576c6..0000000 --- a/www/extensions/servers/serverlist.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "extension.html" %} - -{% block head %} - -{% endblock %} - -{% block body %} - {% if user.is_admin %} - - {% endif %} - -
- {% for s in servers | reverse %} -
- {% if s.url %} -

{{s.name}}

- {% else %} -

{{s.name}}

- {% endif %} -

IP: {{s.ip}}

-

- {{s.description | striptags() | truncate(128, true, "")}} - ... -

-
- {% else %} -

This page is currently under development and will be available soon!

-

No servers have been added yet, please check back later.

- {% endfor %} -
-{% endblock %} diff --git a/www/extensions/servers/static/index.css b/www/extensions/servers/static/index.css deleted file mode 100644 index 647fd51..0000000 --- a/www/extensions/servers/static/index.css +++ /dev/null @@ -1,54 +0,0 @@ -.content { - padding: var(--margin-normal); -} - -/* General server item */ -.content .server > p:nth-child(1) { - font-weight: bold; - font-size: larger; - margin-bottom: 0; -} -.content .server > p:nth-child(1) > a { - text-decoration: none; - color: var(--color-link) -} - -/* Serverlist */ -.content > .serverlist { - width: 100%; - display: flex; - flex-wrap: wrap; -} -.content > .serverlist > .server { - width: 50%; - height: auto; - overflow: hidden; - box-sizing: border-box; - border-bottom: 1px dashed black; - padding: var(--margin-normal); -} -.content > .serverlist > .server:nth-child(2n -1) { - border-right: 1px dashed black; -} -.content > .serverlist > .server > p:nth-child(1) { - margin-top: var(--margin-small); -} -.content > .serverlist > .server > p:nth-child(2) { - margin-top: 0; - margin-left: 5px; - font-size: 15px; -} -.content > .serverlist > .server > p:nth-child(3) { - margin-bottom: var(--margin-small); -} -.content > .serverlist > .server > p:nth-child(3) > a { - font-size: 18px; - font-weight: bold; - text-decoration: none; - color: var(--color-link); - margin-top: auto; - margin-bottom: 0; - bottom: 0; - top: auto; - height: auto; -} diff --git a/www/extensions/servers/tables.js b/www/extensions/servers/tables.js deleted file mode 100644 index 012c60f..0000000 --- a/www/extensions/servers/tables.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = (Tables) => {return class extends Tables { - tables = { - 'server': 0 - } - - server = { - 0:()=>{ - this.db.createTable('server', [ - 'id INTEGER PRIMARY KEY AUTOINCREMENT', - 'admin_id INTEGER NOT NULL', - 'name VARCHAR NOT NULL', - 'description TEXT NOT NULL', - 'ip VARCHAR NOT NULL', - 'url VARCHAR', - 'CONSTRAINT fk_admin_id FOREIGN KEY (admin_id) REFERENCES user(id)' - ]) - } - } -}} diff --git a/www/index.js b/www/index.js deleted file mode 100644 index d5cc3e3..0000000 --- a/www/index.js +++ /dev/null @@ -1,69 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -var extensions = [ - 'profile','servers','nothing','admin','chat' -] - -var root -var data, wg_config -exports.init = function (global) { - ({data,wg_config,texts,nj,config} = global) - nj.addGlobal('dicebear_host', config.dicebear_host) - nj.addGlobal('client_location', config.client_location) - - root = new ((require(`./extensions/root/index.js`))(global.Extension))(global, `${__dirname}/extensions/root/`, `${__dirname}/../data/root/`) - - var extension_indices = {} - for (path of extensions) { - let ext = new ((require(`./extensions/${path}/index.js`))(global.Extension))(global, `${__dirname}/extensions/${path}/`, `${__dirname}/../data/${path}/`) - extension_indices[path] = ext - } - extensions = extension_indices - admin_extensions = {} - for (const ext of Object.keys(extensions)) { - if (extensions[ext].admin_only) { - admin_extensions[ext] = extensions[ext] - delete extensions[ext] - } - } -} - -exports.main = function (req, res) { - var location = req.path.shift() - - // set request context - req.context = {...req.args} - req.context.extensions = extensions - req.context.location = location - - // Authenticate using user&pass, else using ip - data.authenticate(req.headers.authorization, req.ip, wg_config.subnet, function (user, err) { - req.context.user = user - if (err) req.context.auth_err = err - if (user && user.is_admin) req.context.extensions = {...req.context.extensions, ...admin_extensions} - - // Extension - if (location in req.context.extensions) { - ext = req.context.extensions[location] - // If login required - if (!user && ext.requires_login(req.path)) { - res.writeHead(307, {Location: "/login"}) - res.end() - return - } - if (user && !user.is_admin && ext.requires_admin(req.path)) { - res.writeHead(307, {Location: "/"}) - res.end() - return - } - ext.handle_req(req, res) - } - // Root extension - else { - req.path.unshift(location) - root.handle_req(req, res) - } - }) -}