diff --git a/.env.example b/.env.example index 6523d19..6eb14e2 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,14 @@ SECRET_SALT="dummy text" -# Default chatroom settings -DEFAULT_THEME="dark" -DEFAULT_ROOM="hallway" -DEFAULT_INLINE_PREVIEW=true - -# Port configuration -HTTP_PORT=80 -HTTPS_PORT=443 - # Set your privkey and server cert here (don't confuse this with your CSR) ENCRYPTION_HTTPS_PRIV_KEY= ENCRYPTION_HTTPS_CERT= -# Server listening port +ROOMS="Hallway,Español,Français,Deutsche,中文,हिन्दी,TheBackrooms" +DEFAULT_THEME=light +DEFAULT_INLINE_PREVIEW=true + +ADDRESS=0.0.0.0 +HTTP_PORT=80 +HTTPS_PORT=443 USE_HTTPS=false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1ff7704 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5b21339..be59f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,9 @@ TODO # next.js build output .next + +# keys and certs for https +*.key +*.crt +*.csr +*.pem diff --git a/ChatCrypto.js b/ChatCrypto.js deleted file mode 100644 index 9f39f7c..0000000 --- a/ChatCrypto.js +++ /dev/null @@ -1,44 +0,0 @@ - -const crypto = require("crypto") -const Config = require("./classes/Config") - -let chatCrypto = { - CRYPTO_ITERATIONS: 10000, - SALT: Config.SECRET_SALT, - - HASH_LENGTH: 6, - DIGEST: "sha256", - - KEY_LENGTH: 30, - TRIPCODE_LENGTH: 6, - EXPECTED_TOKEN_LENGTH: 60 -} - -/** - * Generate tripcode - */ -chatCrypto.genTripcode = (password) => { - return new Promise(function (resolve, reject) { - crypto.pbkdf2( - password, - chatCrypto.SALT, - chatCrypto.CRYPTO_ITERATIONS, - chatCrypto.HASH_LENGTH, - chatCrypto.DIGEST, - - (error, hash) => { - if (error) { - reject(error) - } else { - resolve(hash.toString("base64").substr(0, chatCrypto.TRIPCODE_LENGTH)) - } - }, - ) - }) -} - -chatCrypto.dispenseToken = () => { - return crypto.randomBytes(chatCrypto.KEY_LENGTH).toString("hex") -} - -module.exports = chatCrypto diff --git a/app.js b/app.js deleted file mode 100755 index 634c8b6..0000000 --- a/app.js +++ /dev/null @@ -1,57 +0,0 @@ -const createError = require("http-errors") -const express = require("express") -const path = require("path") -const bodyParser = require("body-parser") - -const config = require("./classes/Config") -const chatRouter = require("./routes/chat") - -let app = express() - -// view engine setup -app.set("views", path.join(__dirname, "views")) -app.set("view engine", "ejs") - -config.loadConfig() - .then(() => { - app.locals.config = config - }) - .catch((error) => { - console.error(error) - exit(1) - }) - -// NOTE might have to blacklist things like robots.txt and this from /room names -app.use('/favicon.ico', express.static(path.join(__dirname, 'public', 'images', 'favicon.ico'))); - -// JSON engine ExpressJS -app.use(express.json()) -app.use(express.urlencoded({ extended: false })) - - -app.use(express.static(path.join(__dirname, "public"))) // NOTE might have to switch to _ prefix to support /rooms -app.use("/", chatRouter) - - -// catch 404 and forward to error handler -app.use((req, res, next) => { - next(createError(404)) -}) - -// error handler -app.use((err, req, res, next) => { - // set locals, only providing error in development - res.locals.message = err.message - res.locals.error = req.app.get("env") === "development" ? err.status : {} - - if (req.body && req.body.token) { - return chatRouter.errorWithPostToken(err, req, res) - } - - // render the error page - res.status(err.status || 500) - res.render("layout", { page: "error", url: "" }) -}) - -console.log("Express app done setting up"); -module.exports = app diff --git a/bin/www.js b/bin/www.js deleted file mode 100644 index 261b335..0000000 --- a/bin/www.js +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -let fs = require('fs'); -let app = require('../app'); -let debug = require('debug')('zc:server'); - - -/** - * Get port from environment and store in Express and create the HTTP server - */ - -let http = require('http'); - -let httpPort = normalizePort(process.env.HTTP_PORT || 8000); -app.set('httpPort', httpPort); - -let httpServer = http.createServer(app); -httpServer.keepAliveTimeout = 0 // Defaults to 5 seconds, override -httpServer.headersTimeout = 0 // Defaults to 60000 ms, override -httpServer.timeout = 0 -httpServer.setTimeout(0) - -httpServer.listen(httpPort); -httpServer.on('error', onError.bind(null, httpPort)); -httpServer.on('listening', onListening.bind(null, httpServer)); - - -/** - * Get port from environment and store in Express and create the HTTPS server - */ - - -if (process.env.USE_HTTPS.toLowerCase() === 'true') { - - let https = require('https'); - - let privateKey = fs.readFileSync(process.env.ENCRYPTION_HTTPS_PRIV_KEY, 'utf8'); - let certificate = fs.readFileSync(process.env.ENCRYPTION_HTTPS_CERT, 'utf8'); - let credentials = { key: privateKey.replace(/\\n/gm, '\n'), cert: certificate.replace(/\\n/gm, '\n') }; - - let httpsPort = normalizePort(process.env.HTTPS_PORT || 8443); - app.set('httpsPort', httpsPort); - - let httpsServer = https.createServer(credentials, app); - httpsServer.keepAliveTimeout = 0 - httpsServer.headersTimeout = 0 - httpsServer.timeout = 0 - httpsServer.setTimeout(0) - - httpsServer.listen(httpsPort); - httpsServer.on('error', onError.bind(null, httpsPort)); - httpsServer.on('listening', onListening.bind(null, httpsServer)); -} - - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - let port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(port, error) { - if (error.syscall !== 'listen') { - throw error; - } - - let bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening(server) { - let addr = server.address(); - let bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - console.info('Listening on ' + bind + ' => http' + (server.cert ? 's' : '') + "://localhost:" + bind.split(' ')[1]); -} diff --git a/classes/Config.js b/classes/Config.js deleted file mode 100644 index 26f3c53..0000000 --- a/classes/Config.js +++ /dev/null @@ -1,63 +0,0 @@ -const fs = require('fs') -const dotenv = require('dotenv').config() - -let config = { - DEFAULT_THEME: process.env.DEFAULT_THEME, - DEFAULT_ROOM: process.env.DEFAULT_ROOM, - - DEFAULT_INLINE_PREVIEW: process.env.DEFAULT_INLINE_PREVIEW.toLowerCase() === 'true', - - HTTP_PORT: parseInt(process.env.HTTP_PORT), - HTTPS_PORT: parseInt(process.env.HTTPS_PORT), - - SECRET_SALT: process.env.SECRET_SALT, - themes: [], - urlPrefix: '/' // not yet a variable -} - -config.isValidTheme = (theme) => { - if (config.themes.indexOf(theme) != -1) { - return true - } else { - return false - } -} - -config.loadThemes = () => { - return new Promise((resolve, reject) => { - try { - fs.readdir("public/themes", (err, files) => { - config.themes = files.map(file => { - if (file.endsWith(".css")) { - return file.substr(0, file.length - 4) - } - }); - - if (config.isValidTheme(config.DEFAULT_THEME)) { - console.log("Themes loaded: " + config.themes); - resolve(config.themes) - } else { - throw new Error("Default theme '" + config.DEFAULT_THEME + "' does not exist!") - } - }); - } catch (error) { - reject(error) - } - }) -} - -config.loadConfig = () => { - return new Promise((resolve, reject) => { - if (config.urlPrefix === '/') config.urlPrefix = '' - - Promise.all([config.loadThemes()]) - .then(() => { - resolve() - }) - .catch((error) => { - reject(error) - }) - }) -} - -module.exports = config diff --git a/classes/User.js b/classes/User.js deleted file mode 100644 index d17e122..0000000 --- a/classes/User.js +++ /dev/null @@ -1,90 +0,0 @@ -const ChatCrypto = require("../ChatCrypto") -const Config = require("./Config") - -// TODO: User input data needs sanitizing -/** - * User objects hold user info for use in chat - */ -module.exports = class User { - static DEFAULT_THEME = "dark" - - static placeholderSequence = [ - "Type here, press [ENTER] to send...", - "Press [TAB] to type another message here...", - "" - ] - - constructor(handle, pass, res, theme, inlineView, room) { - this.handle = handle // screen name - this.token = ChatCrypto.dispenseToken() // session token - this.tripcode = ChatCrypto.genTripcode(pass) // identifying tripcode - this.theme = theme // preferred theme - this.inlineView = (inlineView ? true : undefined) - this.room = room // the room the user is in - this.res = { "chatroom": res, "post": null, "upload": null, "messages": null, "settings": null } // response object - this.placeholderIter = 0 - this.joinTimeoutInterval = null - } - - toString() { - return '{"handle":"' + this.handle + '","tripcode":"' + this.tripcode + '"}' - } - - // Update the user's current theme live - setTheme(theme) { - if (Config.isValidTheme(theme)) { - this.theme = theme - this.updateStream(``) - } else { - throw new Error("Invalid theme '" + theme + "'") - } - } - - // Attempt to disconnect each page - disconnect() { - if (this.res.chatroom) { - try { - this.res.chatroom - } catch (error) { } - } - if (this.res.post) { - try { - this.res.post - } catch (error) { } - } - if (this.res.upload) { - try { - this.res.upload - } catch (error) { } - } - if (this.res.messages) { - try { - this.res.messages - } catch (error) { } - } - if (this.res.settings) { - try { - this.res.settings - } catch (error) { } - } - } - - nextMsgPlaceholder(req) { - let result = User.placeholderSequence[this.placeholderIter] - - // Prepare for next time the placeholder is requested - if (this.placeholderIter === 0 /*&& req.headers['user-agent'].indexOf("Firefox") !== -1*/) { - this.placeholderIter++ - } else { - this.placeholderIter = 2 - } - - return result - } - - // Update the open chatroom stream with HTML for this user - updateStream(html) { - this.res.messages.write(html) - } - -} diff --git a/classes/User.ts b/classes/User.ts new file mode 100644 index 0000000..44a2536 --- /dev/null +++ b/classes/User.ts @@ -0,0 +1,112 @@ +const express = require('express'); +import { Request, Response } from 'express'; + +const Security = require("../utils/security") +const Config = require("../utils/configSetup") + +type ChatStreams = { + chatroom: Response, + post: Response | null, + upload: Response | null, + chat: Response | null, + settings: Response | null +} + +// TODO: User input data needs sanitizing +/** + * User objects hold user info for use in chat + */ +module.exports = class User { + static DEFAULT_THEME = "dark" + + handle: String + token: String + tripcode: String + theme: String + inlineView: Boolean | undefined + room: Object + frames: ChatStreams + currPlaceholder: number + joinTimeoutInterval: number | null + + static placeholderSequence = [ + "Type here, press [ENTER] to send...", + "Use [TAB] so you don't need to click here to type...", + "Press [TAB] after sending to type another message here...", + "" + ] + + constructor(handle: String, pass: String, chatSession: Response, theme: String, inlineView: Boolean, room: String) { + this.handle = handle // screen name + this.token = Security.dispenseToken() // session token + this.tripcode = Security.genTripcode(pass) // identifying tripcode + this.theme = theme // preferred theme + this.inlineView = (inlineView ? true : undefined) + this.room = room // the room the user is in + this.frames = { "chatroom": chatSession, "post": null, "upload": null, "chat": null, "settings": null } // response object + this.currPlaceholder = 0 + this.joinTimeoutInterval = null + } + + toString() { + return JSON.stringify({ handle: this.handle, tripcode: this.tripcode }) + } + + // Update the user's current theme live + setTheme(theme: String) { + if (Config.isValidTheme(theme)) { + this.theme = theme + this.updateStream(``) + } else { + throw new Error("Invalid theme '" + theme + "'") + } + } + + // Attempt to disconnect each page + disconnect() { + if (this.frames.chatroom) { + try { + this.frames.chatroom.end() + } catch (error) { } + } + if (this.frames.post) { + try { + this.frames.post.end() + } catch (error) { } + } + if (this.frames.upload) { + try { + this.frames.upload.end() + } catch (error) { } + } + if (this.frames.chat) { + try { + this.frames.chat.end() + } catch (error) { } + } + if (this.frames.settings) { + try { + this.frames.settings.end() + } catch (error) { } + } + } + + nextMsgPlaceholder(request: Request) { + let result = User.placeholderSequence[this.currPlaceholder] + + // Prepare for next time the placeholder is requested + if (this.currPlaceholder <= User.placeholderSequence.length - 1) { + this.currPlaceholder += 1 + } + + return result + } + + // Update the open chatroom stream with HTML for this user + updateStream(html: String) { + if (this.frames.chat != null) { + this.frames.chat.write(html) + } + } + +} diff --git a/docker-compose.yml b/docker-compose.yml index a0af099..e98db65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,8 @@ services: context: . dockerfile: Dockerfile ports: - - 8080:80 + - 3000:80 + - 4443:443 volumes: - .:/zerochat:ro tty: true diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..52fc114 --- /dev/null +++ b/index.ts @@ -0,0 +1,88 @@ +const createError = require('http-errors') +const express = require('express'); +import { Request, Response } from 'express'; +const path = require('path'); +const Config = require('./utils/configSetup') +const http = require('http'); +const https = require('https'); +const fs = require('fs'); + +const bodyParser = require('body-parser') +const chatRouter = require('./routes/chat') + +let app = express() + +// view engine setup +app.set("views", path.join(__dirname, "views")) +app.set("view engine", "ejs") + +Config.loadConfig() + .then(() => { + app.locals.config = Config + }) + .catch((error: Error) => { + console.error(error) + process.exit(1) + }) + +app.use('/favicon.ico', express.static(path.join(__dirname, 'public', 'images', 'favicon.ico'))); +app.use('/robots.txt', express.static(path.join(__dirname, 'public', 'robots.txt'))); + +app.use(express.urlencoded({ extended: false })) + +app.use(express.static(path.join(__dirname, 'public'))) +app.use('/', chatRouter) + +// Catch 404s and pass to error handler +app.use((req: Request, res: Response, next: Function) => { + next(createError(404)) +}) + +// Error handler +app.use((err: any, req: Request, res: Response, next: Function) => { + // Set locals, only providing error in development + res.locals.message = err.message + res.locals.error = {} + + // Render the error page + res.status(err.status || 500) + res.render('layout', { page: 'error', url: '' }) +}) + +// Host address +const host = Config.HOST_ADDRESS || '0.0.0.0' + +if (Config.USE_HTTPS === 'true') { // Using HTTPS + if (!Config.HTTPS_PRIV_KEY) { throw new Error("Using HTTPS, but missing HTTPS_PRIV_KEY path") } + if (!Config.HTTPS_CERT) { throw new Error("Using HTTPS, but missing HTTPS_CERT path") } + const credentials = { + key: fs.readFileSync(Config.HTTPS_PRIV_KEY, 'utf8'), + cert: fs.readFileSync(Config.HTTPS_CERT, 'utf8') + }; + const httpsServer = https.createServer(credentials, app); + httpsServer.listen(Config.HTTPS_PORT, (err: any) => { + if (err) { + console.error(err) + process.exit(1) + } + console.log(`Server listening at https://${host}:${Config.HTTPS_PORT}`) + }) + + // Redirect HTTP to HTTPS + const httpRedirect = express(); + httpRedirect.all('*', (req: Request, res: Response) => { + res.redirect(300, `https://${req.hostname}:${Config.HTTPS_PORT}${req.url}`) + }); + const httpServer = http.createServer(httpRedirect); + httpServer.listen(Config.HTTP_PORT, () => console.log(`HTTP server listening and redirecting on port ${Config.HTTP_PORT}`)); +} else { // Using HTTP + const httpServer = http.createServer(app); + httpServer.listen(Config.HTTP_PORT, (err: any) => { + if (err) { + console.error(err) + process.exit(1) + } + console.log(`Server listening at http://${host}:${Config.HTTP_PORT}`) + }) +} +module.exports = app diff --git a/package-lock.json b/package-lock.json index ef0d463..e59816c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,95 @@ "name": "zerochat", "version": "1.0.0", "dependencies": { - "busboy": "^0.3.1", - "debug": "~4.3.1", "dotenv": "^10.0.0", "ejs": "~3.1.6", "express": "~4.17", + "express-fileupload": "^1.2.1", + "expressjs": "^1.0.1", "http-errors": "~1.8.0", "https": "^1.0.0", - "multer": "^1.4.2" + "ts-node": "^9.1.1" + }, + "devDependencies": { + "@types/express": "^4.17.12", + "@types/node": "^14.14.37", + "typescript": "^4.2.4" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", + "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz", + "integrity": "sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.14.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", + "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "node_modules/@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/accepts": { @@ -41,10 +122,10 @@ "node": ">=4" } }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -173,47 +254,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -246,21 +286,10 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - } + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "node_modules/depd": { "version": "1.1.2", @@ -286,6 +315,14 @@ "node": ">=4.5.0" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", @@ -382,6 +419,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-fileupload": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.2.1.tgz", + "integrity": "sha512-fWPNAkBj+Azt9Itmcz/Reqdg3LeBfaXptDEev2JM8bCC0yDptglCnlizhf0YZauyU5X/g6v7v4Xxqhg8tmEfEA==", + "dependencies": { + "busboy": "^0.3.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -395,6 +443,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/expressjs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/expressjs/-/expressjs-1.0.1.tgz", + "integrity": "sha1-IgMoRpoY31rWFeK3oM6ZXxf7ru8=", + "deprecated": "This is a typosquat on the popular Express package. This is not maintained nor is the original Express package." + }, "node_modules/filelist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", @@ -511,11 +565,6 @@ "node": ">= 0.10" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "node_modules/jake": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", @@ -533,6 +582,11 @@ "node": "*" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -595,69 +649,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/multer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", - "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.1", - "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/multer/node_modules/busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", - "dependencies": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/multer/node_modules/dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "dependencies": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -666,14 +657,6 @@ "node": ">= 0.6" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -698,11 +681,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -760,17 +738,6 @@ "node": ">= 0.6" } }, - "node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -861,6 +828,23 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -877,11 +861,6 @@ "node": ">=0.8.0" } }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -901,6 +880,28 @@ "node": ">=0.6" } }, + "node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -913,10 +914,18 @@ "node": ">= 0.6" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } }, "node_modules/unpipe": { "version": "1.0.0", @@ -926,11 +935,6 @@ "node": ">= 0.8" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -947,16 +951,92 @@ "node": ">= 0.8" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "engines": { - "node": ">=0.4" + "node": ">=6" } } }, "dependencies": { + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", + "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.21.tgz", + "integrity": "sha512-gwCiEZqW6f7EoR8TTEfalyEhb1zA5jQJnRngr97+3pzMaO1RKoI1w2bw07TK72renMUVWcWS5mLI6rk1NqN0nA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/node": { + "version": "14.14.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", + "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", + "dev": true + }, + "@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -974,10 +1054,10 @@ "color-convert": "^1.9.0" } }, - "append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, "array-flatten": { "version": "1.1.1", @@ -1093,46 +1173,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -1156,18 +1196,10 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "depd": { "version": "1.1.2", @@ -1187,6 +1219,11 @@ "streamsearch": "0.1.2" } }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, "dotenv": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", @@ -1277,6 +1314,19 @@ } } }, + "express-fileupload": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.2.1.tgz", + "integrity": "sha512-fWPNAkBj+Azt9Itmcz/Reqdg3LeBfaXptDEev2JM8bCC0yDptglCnlizhf0YZauyU5X/g6v7v4Xxqhg8tmEfEA==", + "requires": { + "busboy": "^0.3.1" + } + }, + "expressjs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/expressjs/-/expressjs-1.0.1.tgz", + "integrity": "sha1-IgMoRpoY31rWFeK3oM6ZXxf7ru8=" + }, "filelist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", @@ -1376,11 +1426,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "jake": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", @@ -1392,6 +1437,11 @@ "minimatch": "^3.0.4" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1433,69 +1483,11 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "multer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", - "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", - "requires": { - "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.1", - "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "dependencies": { - "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", - "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - } - }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "requires": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - } - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1514,11 +1506,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -1563,17 +1550,6 @@ } } }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1659,6 +1635,20 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -1669,11 +1659,6 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1687,6 +1672,19 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1696,21 +1694,17 @@ "mime-types": "~2.1.24" } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1721,10 +1715,10 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/package.json b/package.json index d65f293..f8e12fe 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,24 @@ { "name": "zerochat", - "version": "1.0.0", + "version": "1.1.0", "private": true, "scripts": { - "start": "node ./bin/www.js" + "build": "tsc -p tsconfig.json", + "start": "ts-node index.ts" }, "dependencies": { - "busboy": "^0.3.1", - "debug": "~4.3.1", "dotenv": "^10.0.0", "ejs": "~3.1.6", "express": "~4.17", + "express-fileupload": "^1.2.1", + "expressjs": "^1.0.1", "http-errors": "~1.8.0", "https": "^1.0.0", - "multer": "^1.4.2" + "ts-node": "^9.1.1" + }, + "devDependencies": { + "@types/express": "^4.17.12", + "@types/node": "^14.14.37", + "typescript": "^4.2.4" } } diff --git a/image.png b/public/images/image.png similarity index 100% rename from image.png rename to public/images/image.png diff --git a/image2.png b/public/images/image2.png similarity index 100% rename from image2.png rename to public/images/image2.png diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e69de29 diff --git a/public/stylesheets/base.css b/public/stylesheets/base.css index 9a7bdd7..0298a2d 100644 --- a/public/stylesheets/base.css +++ b/public/stylesheets/base.css @@ -10,7 +10,7 @@ html,body{ } *{ font-family: monospace; - font-size: 18px; + font-size: 16px; color: var(--fg); } a{ @@ -25,8 +25,8 @@ input, textarea, select, label[for=fileupload], .forCheckbox{ padding: initial; border: 2px solid var(--border); background-color: var(--bg); - padding: 9px; - border-radius: 3px; + padding: 7px; + border-radius: 4px; display: inline-block; position: relative; margin: 0 2px; @@ -47,6 +47,12 @@ input[type=checkbox] + .forCheckbox { input[type=checkbox]:checked + .forCheckbox { color: var(--fg); } +input[type=checkbox] + .forCheckbox::after { + content: " "; +} +input[type=checkbox]:checked + .forCheckbox::after { + content: "🗸"; +} input[type=submit]{ cursor: pointer; } @@ -58,22 +64,30 @@ select{ background-size: 11px 7px; } input[type=file]{ - /* height: 30px; - padding: 6px; - width: 365px; - font-size: 14px; */ - display: none; + letter-spacing: -1px; + width: 270px; +} +input[type=file]::file-selector-button, input[type=file]::-webkit-file-upload-button { + letter-spacing: -1px; + background-color: var(--bg); + color: var(--fg); + border: 1px solid var(--fg); + border-radius: 3px; + font-family: monospace; +} +input[type=file]::file-selector-button:hover, input[type=file]::-webkit-file-upload-button:hover { + background-color: var(--fg); + color: var(--bg); } label[for=fileupload]{ width: auto; } #uploadsubmit { display: flex; - flex-direction: row-reverse; - width: 200px; + width: 385px; } #uploadsubmit > input[type=submit] { - width: 50%; + width: auto; } #uploadsubmit > label { width: 50%; @@ -89,16 +103,23 @@ label[for=fileupload]{ content: "Uploading..."; transition: .4s ease all!important; } +/* For chrome/chromium browsers only */ +input[type=file]{ + padding: 5px; +} @-moz-document url-prefix() { /* Firefox doesn't show an arrow (nor the list by default) */ .combolist{ padding-right: 30px; } - .combolist:hover,.combolist:active,.combolist:focus{ + .combolist:hover, .combolist:active, .combolist:focus{ background-image: var(--dropdown-arrow); background-repeat: no-repeat; background-position: 95% 50%; background-size: 11px 7px; } + input[type=file]{ + padding: 7px; + } } form { line-height: 50px; @@ -125,15 +146,15 @@ code { #settings{ position: absolute; top: 0px; - right: 0px; + left: 0px; padding: 0px 18px; z-index: 1; text-align: right; background: var(--settings); border: 2px solid var(--border); + border-left: 0; border-top: 0; - border-right: 0; - border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; line-height: initial; } #settings_options, #checkshow{ @@ -258,39 +279,30 @@ label[for=room]{ } /* Message posting page */ - -div#post-frame-parent { +iframe#post-frame{ z-index: 1; - display: grid; - width: calc( 300px + 50vw ); - max-width: calc( 100% - 20px ); - margin: 0 auto; - grid-auto-flow: column; - grid-template-columns: calc(100% - 210px) 210px; - justify-items: center; - align-items: center; - justify-content: center; position: relative; - left: 8px; -} -iframe#post-frame, iframe#upload-frame{ height: 60px; width: 100%; margin: 0 auto; } iframe#settings-frame { + z-index: 2; position: fixed; - right: 0; + left: 0; top: 0; height: 300px; width: 265px; } #post{ - margin: 0 auto; display: flex; - justify-content: center; align-items: center; height: 100%; + width: calc( 300px + 50vw ); + max-width: calc( 100% - 20px ); + margin: 0 auto; + left: -8px; + position: relative; } #upload{ margin: 0 auto; @@ -312,17 +324,15 @@ iframe#chat-frame{ } #chat{ - padding: 10px; - font-size: 18px; width: calc( 300px + 50vw ); max-width: calc( 100% - 20px ); - margin: 50px auto 0 auto; + margin: 60px auto 10px auto; display: flex; flex-direction: column-reverse; } .message { - padding: 10px 5px; + padding: 8px 5px; border-top: 1px solid var(--message-separator); display: flex; } @@ -351,19 +361,101 @@ iframe#chat-frame{ .message.self > .handle{ color: var(--self-color) } +.message:hover{ + background-color: var(--bg-alt); +} -.message .filedownload > :nth-child(2) { - margin-top: 10px; - max-width: 25rem; - max-height: 25rem; +.filedownload{ + display: grid; + grid-auto-flow: row; + justify-content: center; + padding: 0px; } -.message .filedownload > img:hover { - max-width: 100vw; - max-height: initial; - width: 100%; +.message .filedownload > span { + margin: 0 0 -7px 0; + min-width: 0px; + position: absolute; + opacity: 0; + font-size: 14px; +} +.message .filedownload:target > span { + position: fixed; + margin-bottom: 0px; + color: white; + z-index: 1; + width: 100%; + height: initial; + left: 0; + bottom: 9px; + text-align: center; + opacity: 1; + transition: 0.3s ease all; } +.message .filedownload > a > img, .message .filedownload video { + /* margin-top: 5px; */ + max-width: 20rem; + max-height: 20rem; +} + +.message .filedownload img,.message .filedownload video,.message .filedownload audio { + border-radius: 4px; +} +.message .filedownload img { + cursor: pointer; +} +@keyframes popIn { + from { + max-width: 0; + max-height: 0; + opacity: 0; + } + to {} +} +.message .filedownload:target a.previewOpen img { + max-width: calc(100vw - 130px); + max-height: calc(100vh - 130px); + position: fixed; + margin: 0; + display: block; + left: 50%; + top: calc(50% + 30px); + transform: translate(-50%,-50%); + cursor: initial; + border-radius: 0px; + animation: 0.3s popIn ease 1; + /* animation-timing-function: cubic-bezier(0.25,0.1,0.25,1.5); */ +} +.message .filedownload a.previewClose { + pointer-events: none; + display: none; + position: fixed; + margin: 0; + background: rgba(0,0,0,0.85); + transition: 0.2s; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + transition: 0.2s ease opacity; + opacity: 0; + min-width: 100vw; + min-height: 100vh; +} +@keyframes fadeIn { + from {opacity: 0;} + to {opacity: 1;} +} +.message .filedownload:target a.previewClose { + pointer-events: initial; + display: block; + opacity: 1; + animation: fadeIn 0.3s 1; +} +.message .filedownload:target { + z-index: 1; +} .message > div { display: flex; flex-direction: column; @@ -374,6 +466,10 @@ iframe#chat-frame{ color: var(--system-color); } +.filedownload + .message-text { + margin-top: 10px; +} + .error-message { display: flex; justify-content: center; @@ -386,32 +482,7 @@ iframe#chat-frame{ float: left; } -.filedownload{ - display: grid; - grid-auto-flow: row; - justify-content: center; - align-items: center; - justify-items: center; - border: 2px solid var(--border); - border-radius: 3px; - padding: 10px; -} - -.filedownload span { - margin: 0; -} - - -@media only screen and (max-width: 1100px) { - /* { - font-size: 14px; - } - input, textarea, label[for="fileupload"], .forCheckbox { - padding: 5px; - } - select { - padding: 5px 12px 5px 5px; - } */ +@media only screen and (max-width: 1200px) { form { gap: 0px; } @@ -423,7 +494,7 @@ iframe#chat-frame{ width: 230px; } #loginform > input[type="submit"] { - width: 253px; + width: 252px; } #loginform { display: grid; @@ -455,16 +526,35 @@ iframe#chat-frame{ .chat{ font-size: 14px; } + input[type="file"]{ + width: auto; + padding: 7px 0 7px 5px; + } + input[type=file]::file-selector-button, input[type=file]::-webkit-file-upload-button { + display: none; + } + #uploadsubmit { + display: flex; + width: 130px; + } + @-moz-document url-prefix() { /* Firefox doesn't show an arrow (nor the list by default) */ + #uploadsubmit { + width: 155px; + } + } + form#post * { + font-size: 14px; + } } /* Smaller text for smaller screen */ @media only screen and (max-width: 600px) { + *{ + font-size: 14px; + } div#post-frame-parent{ grid-template-columns: calc(100% - 210px) 130px; } - #chat * { - font-size: 14px; - } .message { padding: 7px 5px; } @@ -473,26 +563,6 @@ iframe#chat-frame{ max-height: 100%; } } -@media only screen and (max-width: 500px) { - div#post-frame-parent{ - grid-template-columns: calc(100% - 130px) 130px; - } -} -@media only screen and (max-width: 199px) { - #uploadsubmit *, #uploadsubmit label[for="fileupload"] { - font-size: 12px; - padding: 5px; - } - .error-message{ - font-size: 12px; - } -} -@media only screen and (max-width: 370px) { - #messagebox { - font-size: 12px; - padding: 5px; - } -} @media only screen and (max-width: 1670px) { #settings-frame.snapbottom { @@ -509,7 +579,7 @@ iframe#chat-frame{ padding: 0px 10px; border-top-width: 2px; border-right-width: 2px; - border-bottom-left-radius: initial; + border-bottom-right-radius: initial; border-top-right-radius: 3px; top: inherit; right: inherit; diff --git a/public/themes/dark.css b/public/themes/dark.css index 3f4cd41..594f473 100644 --- a/public/themes/dark.css +++ b/public/themes/dark.css @@ -1,8 +1,9 @@ :root { - --bg: #101010; + --bg: #222222; + --bg-alt: #212121;; --fg: #c0c0c0; --border: #f0f0f0bd; - --message-separator: #88888833; + --message-separator: #88888822; --handle-color: #fa8072; --self-color: #4682b4; --system-color: #46b498; diff --git a/public/themes/light.css b/public/themes/light.css index 554defd..6e1a79f 100644 --- a/public/themes/light.css +++ b/public/themes/light.css @@ -1,8 +1,9 @@ :root { --bg: #efefef; + --bg-alt: #e2e2e2; --fg: #101010; --border: #000000bd; - --message-separator: #88888833; + --message-separator: #88888822; --handle-color: #fa8072; --self-color: #4682b4; --system-color: #25a771; diff --git a/rooms.txt b/rooms.txt deleted file mode 100644 index 59f2e34..0000000 --- a/rooms.txt +++ /dev/null @@ -1,7 +0,0 @@ -Hallway -Non English chat -Chat-room-1 -Chat-room-2 -Chat-room-3 -Just-another-room -Others \ No newline at end of file diff --git a/routes/chat.js b/routes/chat.js deleted file mode 100644 index 4d18941..0000000 --- a/routes/chat.js +++ /dev/null @@ -1,646 +0,0 @@ -const express = require("express") -const fs = require('fs') -const User = require("../classes/User") -const Config = require('../classes/Config') -const ChatCrypto = require('../ChatCrypto') - -const Busboy = require('busboy') - -// The exact amount of bytes (1024) needed for a browser to take our (incomplete) response seriously -// and begin rendering the HTML sent so far, immediately -const WHITESPACE_BITS = " ".repeat(1024) - -const MAX_MESSAGE_LENGTH = 300 -const MAX_FILE_SIZE = 5242880 // 5 Mb -const MAX_HANDLE_LENGTH = 15 -const MAX_PASSCODE_LENGTH = 64 -const MAX_ROOMNAME_LENGTH = 24 -const URL_PREFIX = Config.urlPrefix // TODO Support any prefix, replace all / in ejs files with var from cfg file -const CONNECT_DELAY_TIMEOUT = 20000 // How long to wait before disconnecting user if they never get past chatroom page - -const ROUTES = { - MAIN: '', - POST_MESSAGE: '_post-message', - UPLOAD_FILE: '_upload-file', - CHAT_MESSAGES: '_view-messages', - SETTINGS: '_settings', -} -const PATHS = Object.values(ROUTES) // Array containing only the values of ROUTES - -const VIEWS = { - LAYOUT: 'layout', - FRONT_PAGE: 'index', - CHATROOM: 'chatroom', - ERROR: 'error', - WRITE_MESSAGE: 'write-message', - UPLOAD: 'upload-file', - NEW_MESSAGE: 'new-message', - VIEW_MESSAGES: 'view-messages', - ERROR_MESSAGE: 'error-message', - SETTINGS: 'settings', -} - -const ERRORS = { - INVALID_TOKEN: { message: "Invalid Token", error: "401" }, - INVALID_REQUEST: { message: "Invalid Request", error: "400" }, -} - -let defaultRoom = Config.DEFAULT_ROOM -const ROOMS_FILE = "rooms.txt" -const FALLBACK_DEFAULT_ROOM = [defaultRoom] - -let roomsList = [] -let users = [] -let router = express.Router() - -const getRooms = () => { - return new Promise((resolve, reject) => { - fs.readFile(ROOMS_FILE, 'utf8', (err, data) => { - if (err) throw err - - if (data == null || data == []) { - console.error("Error: " + ROOMS_FILE + " is empty or invalid, it should be a list of default rooms seperated by lines. Defaulting to /" + FALLBACK_DEFAULT_ROOM[0]) - reject() - process.exit(1) - } else { - resolve(data.split(/\r?\n/)) - } - }) - }) -} - -const broadcast = (user, message, room, file = undefined) => { - return new Promise((resolve, reject) => { - - // A second check before sending. It should not be possible to hit this branch - if ((typeof message != "string" || message.trim() == '') && !file) { - reject("Message is blank or invalid") - console.warn("User managed to send a post request with no file or message and pass inital checks!") - return user.disconnect() - } - - let fileData = { - buffer: undefined, - type: undefined, - name: undefined, - mimetype: undefined - } - - if (file) { - if (file.buffer && file.buffer.length !== 0) { - fileData.buffer = file.buffer.toString("base64") - fileData.type = file.mimetype.substr(0, file.mimetype.indexOf('/')) - fileData.name = file.originalname - fileData.mimetype = file.mimetype - } else { - return false - } - } - - for (const iUser of users) { - if (iUser.room !== room) { - continue - } - let messageType = "user" - let postHandle = "" - let postTrip = "" - - if (user == null) { - // If the 'user' param is null, that means it's a system message - messageType = "system" - } else { - postHandle = user.handle - postTrip = user.tripcode - } - - if (iUser.res.messages) { - if (user && user.token == iUser.token) { - // Same user who wrote the message - iUser.res.messages.render(VIEWS.NEW_MESSAGE, - { - handle: iUser.handle, - tripcode: iUser.tripcode, - messageType: "self", - message: message, - inlineView: iUser.inlineView, - timestamp: new Date().toUTCString(), - file: fileData.buffer, - filetype: fileData.type, - filename: fileData.name, - mimetype: fileData.mimetype - }, - (err, html) => { - iUser.res.messages.write(html) - }) - } else { - // Other user in chat - iUser.res.messages.render(VIEWS.NEW_MESSAGE, - { - handle: postHandle, - tripcode: postTrip, - messageType: messageType, - message: message, - inlineView: iUser.inlineView, - timestamp: new Date().toUTCString(), - file: fileData.buffer, - filetype: fileData.type, - filename: fileData.name, - mimetype: fileData.mimetype - }, - (err, html) => { - iUser.res.messages.write(html) - }) - } - } - } - - resolve('success') - }) -} - -let sanitizeRoomName = (room) => { - room = room.trim().toLowerCase().replace(/[\\|\/]/g, "") - room = decodeURI(room) - - if (room.charAt(0) === "_") { - room = room.substr(1).trim() - } - - return room -} - -let disconnectUser = (user) => { - let i = users.indexOf(user) - - if (i > -1) { - userFound = true - let disconnectMsg = users[i].handle + " (" + users[i].tripcode + ") left." - let room = user.room - user.disconnect() - user = null - users.splice(i, 1) - broadcast(null, disconnectMsg, room) - } - - return -} - -let getUserByToken = (token) => { - return users.filter((user) => user.token === token)[0] -} - -let getUserByHandle = (handle, room) => { - let user = undefined - if (room) { - user = users.filter((user) => user.handle === handle && user.room === room)[0] - } else { - user = users.filter((user) => user.handle === handle)[0] - } - return user -} - -router.errorWithPostToken = (err, req, res) => { - let user = getUserByToken(req.body.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: err.message, - user: user, - redirect: URL_PREFIX + ROUTES.UPLOAD_FILE + "?token=" + req.body.token - }) -} - -// Set headers -router.all("*", (req, res, next) => { - res.append("content-type", "text/html; charset=utf-8") - req.socket.setKeepAlive(true) // prevent TCP connection from closing - next() -}) - -// POST REQUEST VALIDATION -// Uses "handle", "passcode", "room", "theme", "url" -router.post("*", async (req, res, next) => { - let upload = new Busboy({ headers: req.headers }) - new Promise((resolve, reject) => { - upload.on('file', function (fieldname, file, filename, encoding, mimetype) { - req.file = { - buffer: new Buffer.alloc(0) - } - let fileLoadedSize = 0 - file.on('data', function (data) { - fileLoadedSize += data.length - console.log("Uploaded so far: " + fileLoadedSize) - if (fileLoadedSize > MAX_FILE_SIZE) { - // File too large - file.emit('end') - return resolve(new Error("File too large")) - } else { - req.file.buffer = Buffer.concat([req.file.buffer, data]) - } - }) - file.on('end', function () { - console.log('File [' + fieldname + '] Finished') - this.removeAllListeners() - req.file.mimetype = mimetype - req.file.encoding = encoding - req.file.originalname = filename - req.file.size = fileLoadedSize - }) - if (parseInt(req.headers['content-length']) > MAX_FILE_SIZE) { - // File too large - file.emit('end') - return resolve(new Error("File too large")) - } - }) - upload.on('field', function (fieldname, value) { - console.log('Field [' + fieldname + ']: value: ' + value) - req.body[fieldname] = value - }) - upload.on('finish', function () { - console.log('Done parsing form!') - return resolve() - }) - }).then((result) => { - let user = getUserByToken(req.query.token) - if (!user) { user = undefined } - if (result instanceof Error) { - console.error("Error in file upload: " + result) - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: result.message, - user: user, - redirect: URL_PREFIX + ROUTES.UPLOAD_FILE + "?token=" + req.query.token - }) - } - - // Login sanitization - if (req.body.handle && req.body.passcode) { - req.body.handle = req.body.handle.trim() - req.body.passcode = req.body.passcode.trim() - req.body.room = sanitizeRoomName(req.body.room) - - // Use the default room if none is specified - if (req.body.room === "" && defaultRoom) { - req.body.room = defaultRoom - } - } - - // Settings sanitization - req.body.setSettings = (req.body.setSettings && req.body.setSettings.toString() == 'true') - req.body.join = (req.body.join && req.body.join.toString() == 'true') - if (req.body.setSettings === true || req.body.join === true) { - req.body.inlineView = (req.body.inlineView ? true : undefined) - req.body.theme = req.body.theme.trim() - // Check if invalid theme - if (!Config.isValidTheme(req.body.theme)) { - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "Invalid theme", - redirect: URL_PREFIX - }) - } - } - - if (req.url.substr(0, (URL_PREFIX + "_").length) !== URL_PREFIX + "_") { - // Check if the user is sending an update to their settings or not - if (req.body.setSettings && req.body.join === undefined) { - // Reload front page with settings applied - return res.render(VIEWS.LAYOUT, { - page: VIEWS.FRONT_PAGE, - handleMaxlen: MAX_HANDLE_LENGTH, - passMaxlen: MAX_PASSCODE_LENGTH, - roomNameMaxlen: MAX_ROOMNAME_LENGTH, - theme: req.body.theme || Config.DEFAULT_THEME, - inlineView: req.body.inlineView, - setSettings: req.body.setSettings, - url: sanitizeRoomName(req.url), - rooms: roomsList - }) - } else if (req.body.handle === "" - || req.body.passcode === "" - || req.body.room === "") { - // User is missing a required field - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "Field missing", - redirect: URL_PREFIX - }) - } else if ((req.body.handle && req.body.handle.length > MAX_HANDLE_LENGTH) - || (req.body.passcode && req.body.passcode.length > MAX_PASSCODE_LENGTH) - || (req.body.room && req.body.room.length > MAX_ROOMNAME_LENGTH)) { - // Disconnect if user is logging in with a name/passcode too long or blank - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "A field you entered was too long", - redirect: URL_PREFIX - }) - } else if (getUserByHandle(req.body.handle, req.body.room) != undefined) { - // Disconnect user is their name is already taken - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "Someone with that handle is already in /" + req.body.room, - redirect: URL_PREFIX - }) - } - } else if (req.url.startsWith(URL_PREFIX + ROUTES.POST_MESSAGE)) { - - // Disconnect if user is sending a blank message without a file, or a message too large - if (req.body.message == null || req.body.message.trim() == "") { - // no message - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "Message required", - user: user, - redirect: URL_PREFIX + ROUTES.POST_MESSAGE + "?token=" + req.query.token - }) - } else if (req.body.message.length > MAX_MESSAGE_LENGTH) { - // message too large - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "Message too long", - user: user, - redirect: URL_PREFIX + ROUTES.POST_MESSAGE + "?token=" + req.query.token - }) - } - } else if (req.url.startsWith(URL_PREFIX + ROUTES.UPLOAD_FILE)) { - let user = getUserByToken(req.query.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - if (req.file != null && req.file.size > MAX_FILE_SIZE) { - // file too large - console.error("Somehow, the FILE TOO LARGE error was not caught!"); - return 1 // FIXME Should have already returned - } else if (req.file == null || req.file.size == 0) { - // file required - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "No file chosen", - user: user, - redirect: URL_PREFIX + ROUTES.UPLOAD_FILE + "?token=" + req.query.token - }) - } - } - next() - }).catch((err) => - console.error(err) - ) - req.pipe(upload) -}) - -/* MAIN LOGIN PAGE */ -let MAIN_LOGIN_REGEX = new RegExp(`${URL_PREFIX}(?!_).*`) // Matches /url but not /_url -router.get(MAIN_LOGIN_REGEX, (req, res, next) => { - req.url = sanitizeRoomName(req.url) - - return res.render(VIEWS.LAYOUT, { - page: VIEWS.FRONT_PAGE, - handleMaxlen: MAX_HANDLE_LENGTH, - passMaxlen: MAX_PASSCODE_LENGTH, - roomNameMaxlen: MAX_ROOMNAME_LENGTH, - theme: req.body.theme || Config.DEFAULT_THEME, - inlineView: (req.body.inlineView === undefined ? Config.DEFAULT_INLINE_PREVIEW : req.body.inlineView), - setSettings: req.body.setSettings, - url: req.url, - rooms: roomsList - }) -}) - -/* IFRAMES PRECHECK */ -router.get(URL_PREFIX + "_*", (req, res, next) => { - // If we're supposed to load an iframe in the chatroom view - if (req.query.token != null && req.query.token.length === ChatCrypto.EXPECTED_TOKEN_LENGTH) { - next() - } else { - // Disconnect if the user is connecting to the upload form or chat without a token in the GET request - return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN, req.destroy()) - } -}) - -/* CHATROOM */ -router.post(URL_PREFIX + ROUTES.MAIN, (req, res, next) => { - let user = null - if (req.body.handle === undefined || req.body.passcode === undefined) { - return res.render(VIEWS.ERROR, ERRORS.INVALID_REQUEST, res.end()) - } - user = new User(req.body.handle, req.body.passcode, res, req.body.theme, req.body.inlineView, req.body.room) - users.push(user) - - // Wait until the tripcode is done generating - user.tripcode.then((trip) => { - user.tripcode = trip - user.res.chatroom.render( - VIEWS.LAYOUT, - { - page: VIEWS.CHATROOM, - user: user, - url: user.room, - postmsg: URL_PREFIX + ROUTES.POST_MESSAGE, - uploadfile: URL_PREFIX + ROUTES.UPLOAD_FILE, - chatmsgs: URL_PREFIX + ROUTES.CHAT_MESSAGES, - settingsPanel: URL_PREFIX + ROUTES.SETTINGS, - snapbottom: true - } - ) - }) - - // Timeout and delete the user if they connect to this page but never connect to the messages iframe - user.joinTimeoutInterval = setInterval(() => { - clearInterval(user.joinTimeoutInterval) - disconnectUser(user) - }, CONNECT_DELAY_TIMEOUT) -}) - -/* POST MSG IFRAME */ -router.get(URL_PREFIX + ROUTES.POST_MESSAGE, (req, res, next) => { - let user = getUserByToken(req.query.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.post = res - - user.res.post.render(VIEWS.LAYOUT, { - page: VIEWS.WRITE_MESSAGE, - user: user, - maxlen: MAX_MESSAGE_LENGTH, - placeholder: user.nextMsgPlaceholder(req) - }, (err, html) => { user.res.post.end(html) }) -}) -/* SUBMITTING POST MSG IFRAME */ -router.post(URL_PREFIX + ROUTES.POST_MESSAGE, (req, res, next) => { - let user = getUserByToken(req.body.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.post = res - - // Show the message to all users who have loaded the chatroom - broadcast(user, req.body.message, user.room) - .then((status) => { - // Went ok - return status - }) - .catch((status) => { - console.error(status); - return status - }) - .finally((status) => { - user.res.post.render(VIEWS.LAYOUT, { - page: VIEWS.WRITE_MESSAGE, - user: user, - maxlen: MAX_MESSAGE_LENGTH, - placeholder: user.nextMsgPlaceholder(req) - }, (err, html) => { return user.res.post.end(html) }) - }) -}) - - -/* UPLOAD FILE IFRAME */ -router.get(URL_PREFIX + ROUTES.UPLOAD_FILE, (req, res, next) => { - let user = getUserByToken(req.query.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.upload = res - - user.res.upload.render( - VIEWS.LAYOUT, - { - page: VIEWS.UPLOAD, - user: user - } - ) -}) -/* SUBMITTING UPLOAD FILE IFRAME */ -router.post(URL_PREFIX + ROUTES.UPLOAD_FILE, (req, res, next) => { - let user = getUserByToken(req.body.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.upload = res - - // Show the file to all users who have loaded the chatroom - broadcast(user, "", user.room, req.file) - .then((status) => { - // Went ok - return status - }) - .catch((status) => { - console.error(status); - return status - }) - .finally((status) => { - user.res.upload.render(VIEWS.LAYOUT, { - page: VIEWS.UPLOAD, - user: user - }, (err, html) => { return user.res.upload.end(html) }) - }) -}) - - -/* SETTINGS IFRAME */ -router.get(URL_PREFIX + ROUTES.SETTINGS, (req, res, next) => { - let user = getUserByToken(req.query.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.settings = res - - user.res.settings.render( - VIEWS.LAYOUT, - { - page: VIEWS.SETTINGS, - user: user, - theme: user.theme, - inlineView: user.inlineView, - setSettings: req.body.setSettings, - snapbottom: true, - redirect: URL_PREFIX + ROUTES.SETTINGS + "?token=" + req.query.token, - }, - (err, html) => { return user.res.settings.end(html) } - ) -}) -/* SUBMITTING SETTINGS IFRAME */ -router.post(URL_PREFIX + ROUTES.SETTINGS, (req, res, next) => { - let user = getUserByToken(req.query.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.settings = res - - if (!req.body.setSettings) { - return res.render(VIEWS.LAYOUT, { - page: VIEWS.ERROR_MESSAGE, - url: "_hidden", - error: "Invalid request", - user: user, - redirect: URL_PREFIX + ROUTES.UPLOAD_FILE + "?token=" + req.query.token - }) - } - - if (user.theme !== req.body.theme || user.inlineView !== req.body.inlineView) { - try { - user.setTheme(req.body.theme) - user.inlineView = req.body.inlineView - user.res.settings.render( - VIEWS.LAYOUT, - { - page: VIEWS.SETTINGS, - user: user, - theme: user.theme, - inlineView: user.inlineView, - setSettings: req.body.setSettings, - snapbottom: true, - setSettings: true, - redirect: URL_PREFIX + ROUTES.SETTINGS + "?token=" + req.query.token, - }, - (err, html) => { return user.res.settings.end(html) } - ) - } catch (error) { - console.error(error) - disconnectUser(user) - } - } -}) - - -/* MESSAGES IFRAME (STREAMED) */ -router.get(URL_PREFIX + ROUTES.CHAT_MESSAGES, (req, res, next) => { - - // Find user - let user = getUserByToken(req.query.token) - if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } - user.res.messages = res - - // On disconnect, remove the user entirely - req.on("close", () => { - disconnectUser(user) - }) - - user.res.messages.render(VIEWS.LAYOUT, { page: VIEWS.VIEW_MESSAGES, user: user }, (err, html) => { - user.res.messages.write(html) - user.res.messages.render(VIEWS.NEW_MESSAGE, - { - handle: "", - tripcode: "", - messageType: "system", - message: "Users online: " + - users - .filter((iUser) => iUser.room === user.room) - .map((iUser) => iUser.handle) - .join(', '), - user: user, - timestamp: new Date().toUTCString() - }, - (err, html) => { - user.res.messages.write(html) - user.res.messages.write(WHITESPACE_BITS) // Firefox needs extra data sent to render the document properly - }) - - broadcast(null, user.handle + " (" + user.tripcode + ") joined /" + user.room + ".", user.room) - - // After having loaded the messages iframe, clear out the interval - if (user.joinTimeoutInterval) - clearInterval(user.joinTimeoutInterval) - - }) -}) - -getRooms().then(data => roomsList = data) -module.exports = router \ No newline at end of file diff --git a/routes/chat.ts b/routes/chat.ts new file mode 100644 index 0000000..cc964c8 --- /dev/null +++ b/routes/chat.ts @@ -0,0 +1,556 @@ +import { Request, Response } from 'express' +const express = require("express") +const fileUploader = require('express-fileupload'); +const User = require("../classes/User") +const ConfigSetup = require('../utils/configSetup') +const { DEFAULT_THEME, DEFAULT_ROOM, ROOMS, DEFAULT_INLINE_PREVIEW, ROUTES, VIEWS, ERRORS } = require('../utils/configSetup') +const Security = require('../utils/security') +// TODO gotta clean up unused things + +// The exact amount of bytes (1024) needed for a browser to take our response +// seriously and begin rendering the HTML sent so far, immediately +const WHITESPACE_BITS = " ".repeat(1024) + +const MAX_MESSAGE_LENGTH = 300 // TODO make some kind of configSetup var +const MAX_FILE_SIZE = 5242880 // 5 Mb // TODO make some kind of configSetup var +const MAX_HANDLE_LENGTH = 15 +const MAX_PASSCODE_LENGTH = 64 +const MAX_ROOMNAME_LENGTH = 24 +const URL_PREFIX = ConfigSetup.urlPrefix // TODO Support any prefix, replace all / in view files with var from cfg file +const CONNECT_DELAY_TIMEOUT = 20000 // How long to wait before disconnecting user if they never get past chatroom page +const PATHS = Object.values(ROUTES) // Array containing only the values of ROUTES + +const BLACKLISTED_ROOM_NAMES = [ + 'robots.txt', + 'favicon.ico', + 'public' +] + +type FileUpload = { + data: Buffer | string | null, + type: string | null, + name: string | null, + mimetype: string | null, + size: number | null, + truncated: boolean | null, + hrefImgId: string | null +} + +interface ZCRequest extends Request { + files: any, // express-fileupload adds this into the request + file: FileUpload, + message: string +} + +// let users:Array> = [] +let users: Array = [] +let router = express.Router() + +const broadcast = async (user: typeof User | null, message: string, room: string, file: FileUpload | null | undefined = null) => { + // A second check before sending. TODO It should not be possible to hit this branch, remove other checks elsewhere? + if (message.trim() === '' && !file) { + console.warn("User managed to send a post request with no file or message and pass initial checks!") + user.disconnect() + throw new Error("Message is blank or invalid") + } + + if (file != null) { + if (file.data && file.data.length !== 0 && file.name && file.mimetype) { + file.data = file.data.toString("base64") + file.type = file.mimetype.substr(0, file.mimetype.indexOf('/')) + file.name = file.name + file.mimetype = file.mimetype + file.hrefImgId = ( file.type == "image" ? Math.random().toString(16) : "" ) + } else { + return false // File is present but one of it's attributes is missing + } + } else { + file = undefined // No file + } + + for (const iUser of users) { + if (iUser.room !== room) continue + + let postHandle = "" + let postTrip = "" + let messageType = "user" + + if (user === null) { + messageType = "system" // If 'user' is null, it's a system message + } else { + if (user && user.token == iUser.token) { + // User receiving own message + postHandle = iUser.handle + postTrip = iUser.tripcode + messageType = "self" + } else { + // Other user receiving this message + postHandle = user.handle + postTrip = user.tripcode + } + } + + if (iUser.frames.chat) { + iUser.frames.chat.render(VIEWS.NEW_MESSAGE, + { + handle: postHandle, + tripcode: postTrip, + messageType: messageType, + message: message, + inlineView: iUser.inlineView, + timestamp: new Date().toUTCString(), + file: file + }, + (err: Error, html: string) => { + iUser.frames.chat.write(html) + }) + } + } +} + +const sanitizeRoomName = (room: string) => { + room = room.trim().toLowerCase().replace(/[\\|\/]/g, "") + room = decodeURI(room) + + while (room.charAt(0) === "_" || room.trim().length < room.length) { + if (room.charAt(0) === "_") room = room.substr(1).trim() + else room = room.trim() + } + + // Use the default room if none is specified + if (room == null) room = '' + + return room +} + +const disconnectUser = (user: typeof User) => { + let i = users.indexOf(user) + + if (i > -1) { + // userFound = true + let disconnectMsg = users[i].handle + " (" + users[i].tripcode + ") left." + let room = user.room + user.disconnect() + user = null + users.splice(i, 1) + broadcast(null, disconnectMsg, room) + } +} + +let getUserByToken = (token: string) => { + return users.filter((user) => user.token === token)[0] +} + +let getUserByHandle = (handle: string, room: string) => { + let user = undefined + if (room) { + user = users.filter((user) => user.handle === handle && user.room === room)[0] + } else { + user = users.filter((user) => user.handle === handle)[0] + } + return user +} + +// Clean up request input +const sanitizeRequest = (req: ZCRequest) => { + // Login sanitization + if (req.body.handle && req.body.passcode) { + req.body.handle = req.body.handle.trim() + req.body.passcode = req.body.passcode.trim() + req.body.room = sanitizeRoomName(req.body.room) + if (req.body.room == "") req.body.room = sanitizeRoomName(DEFAULT_ROOM) + } + // Settings sanitization + req.body.setSettings = (req.body.setSettings && req.body.setSettings.toString() == 'true') + req.body.join = (req.body.join && req.body.join.toString() == 'true') + return req +} + +// Set headers +router.all("*", (req: ZCRequest, res: Response, next: Function) => { + res.append("content-type", "text/html; charset=utf-8") + req.socket.setKeepAlive(true) // prevent TCP connection from closing + next() +}) + +// File upload handling +const filesLimitHandler = (req: ZCRequest, res: Response, next: Function) => { + let user = getUserByToken(req.query.token as string) + if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } + + if (req.files && Object.keys(req.files) && Object.keys(req.files).length > 1) { + // Can only upload one file + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Cannot upload more than one file.", + user: user, + redirect: URL_PREFIX + ROUTES.WRITE_MESSAGE + "?token=" + req.body.token + (req.body.message ? "&msg=" + req.body.message : "") + }) + } else { + // File is too large + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Cannot upload files over " + MAX_FILE_SIZE / 1048576 + " MBs.", + user: user, + redirect: URL_PREFIX + ROUTES.WRITE_MESSAGE + "?token=" + req.body.token + (req.body.message ? "&msg=" + req.body.message : "") + }) + } +} + +router.use(fileUploader({ useTempFiles: false, limits: { files: 1, fileSize: 5242880 }, limitHandler: filesLimitHandler, })); + +// POST REQUEST VALIDATION +// Uses "handle", "passcode", "room", "theme", "url" +router.post("*", async (req: ZCRequest, res: Response, next: Function) => { + req = sanitizeRequest(req) + + if (req.files && req.files.fileupload) { + if (req.files.fileupload.truncated) return; // File is too large + req.file = req.files.fileupload as FileUpload + } + + if (req.body != null && req.body.message != null && req.body.message.trim() != "") + req.message = req.body.message + + let isPosting = false + if (req.url.startsWith(URL_PREFIX + ROUTES.WRITE_MESSAGE)) + isPosting = true + + + let user = getUserByToken(req.query.token as string) || undefined + + // Disconnect if message is too large + if (req.message && req.message.length > MAX_MESSAGE_LENGTH) { + // Message too large + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Message too long.", + user: user, + redirect: URL_PREFIX + ROUTES.WRITE_MESSAGE + "?token=" + req.body.token + (req.body.message ? "&msg=" + req.body.message : "") + }) + } + + if (req.body.setSettings === true || req.body.join === true) { + req.body.inlineView = (req.body.inlineView ? true : undefined) + req.body.theme = req.body.theme.trim() + // Check if invalid theme + if (!ConfigSetup.isValidTheme(req.body.theme)) { + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Invalid theme", + user: user, + redirect: URL_PREFIX + ROUTES.SETTINGS + "?token=" + req.body.token + }) + } + } + + if (!isPosting && (req.file || req.message)) { + // File request IS present, but for the wrong page + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: '_hidden', + error: 'Cannot post to this page.', + user: user, + redirect: URL_PREFIX + }) + } else if (isPosting) { + if (!req.file && !req.message) { + // No file or message present, but needed for the POST method on this page + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: '_hidden', + error: 'Message or file required.', + user: user, + redirect: URL_PREFIX + ROUTES.WRITE_MESSAGE + "?token=" + req.body.token + }) + } + /* Viewing the post page and handling possible file uploads, good to continue */ + return next() + } else if (req.url.substr(0, (URL_PREFIX + "_").length) !== URL_PREFIX + "_") { + // Check whether the user is sending an update to their settings + if (req.body.setSettings && req.body.join === undefined) { + // Reload front page with settings applied + return res.render(VIEWS.LAYOUT, { + page: VIEWS.FRONT_PAGE, + handleMaxlen: MAX_HANDLE_LENGTH, + passMaxlen: MAX_PASSCODE_LENGTH, + roomNameMaxlen: MAX_ROOMNAME_LENGTH, + theme: req.body.theme || ConfigSetup.DEFAULT_THEME, + inlineView: req.body.inlineView, + setSettings: req.body.setSettings, + url: sanitizeRoomName(req.url), + rooms: ROOMS, + defaultRoom: DEFAULT_ROOM + }) + } else if (req.body.handle === "" + || req.body.passcode === "") { + // FIXME Shouldn't this check if 'isPosting' is true? Or is this joining? + // User is missing a required field + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Field missing", + user: user, + redirect: URL_PREFIX + }) + } else if ((req.body.handle && req.body.handle.length > MAX_HANDLE_LENGTH) + || (req.body.passcode && req.body.passcode.length > MAX_PASSCODE_LENGTH) + || (req.body.room && req.body.room.length > MAX_ROOMNAME_LENGTH)) { + // Disconnect if user is logging in with a name/passcode/room too long + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "A field you entered was too long", + user: user, + redirect: URL_PREFIX + }) + } else if (getUserByHandle(req.body.handle, req.body.room) != undefined) { + // Disconnect user is their name is already taken + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Someone with that handle is already in /" + req.body.room, + user: user, + redirect: URL_PREFIX + }) + } + } + next() +}) + +/* MAIN LOGIN PAGE */ +let MAIN_LOGIN_REGEX = new RegExp(`${URL_PREFIX}(?!_).*`) // Matches /url but not /_url +router.get(MAIN_LOGIN_REGEX, (req: ZCRequest, res: Response, next: Function) => { + req.url = sanitizeRoomName(req.url) + if (BLACKLISTED_ROOM_NAMES.includes(req.url)) { + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Invalid room " + req.url, + redirect: URL_PREFIX + }) + } + + return res.render(VIEWS.LAYOUT, { + page: VIEWS.FRONT_PAGE, + handleMaxlen: MAX_HANDLE_LENGTH, + passMaxlen: MAX_PASSCODE_LENGTH, + roomNameMaxlen: MAX_ROOMNAME_LENGTH, + theme: req.body.theme || ConfigSetup.DEFAULT_THEME, + inlineView: (req.body.inlineView === undefined ? ConfigSetup.DEFAULT_INLINE_PREVIEW : req.body.inlineView), + setSettings: req.body.setSettings, + url: req.url, + rooms: ROOMS, + defaultRoom: DEFAULT_ROOM + }) +}) + +/* IFRAMES PRECHECK */ +router.get(URL_PREFIX + "_*", (req: ZCRequest, res: Response, next: Function) => { + // If we're supposed to load an iframe in the chatroom view + let knownRoute = false + const urlPath = req.url.substr(URL_PREFIX.length, (req.url.indexOf('?') != -1 ? req.url.indexOf('?') - 1 : req.url.length)).trim() + for (const key in ROUTES) { + if (urlPath == ROUTES[key]) { + knownRoute = true + } + } + if (!knownRoute) { + return res.render(VIEWS.LAYOUT, { + page: VIEWS.FRONT_PAGE, + handleMaxlen: MAX_HANDLE_LENGTH, + passMaxlen: MAX_PASSCODE_LENGTH, + roomNameMaxlen: MAX_ROOMNAME_LENGTH, + theme: req.body.theme || ConfigSetup.DEFAULT_THEME, + inlineView: (req.body.inlineView === undefined ? ConfigSetup.DEFAULT_INLINE_PREVIEW : req.body.inlineView), + setSettings: req.body.setSettings, + url: sanitizeRoomName(req.url), + rooms: ROOMS, + defaultRoom: DEFAULT_ROOM + }) + } + + if (req.query.token != null && req.query.token.length === Security.EXPECTED_TOKEN_LENGTH) { + return next() + } else { + // Disconnect if the user is connecting to the upload form or chat without a token in the GET request + return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN, () => req.destroy) + } +}) + +/* WRITE MSG IFRAME */ +router.get(URL_PREFIX + ROUTES.WRITE_MESSAGE, (req: ZCRequest, res: Response, next: Function) => { + let user = getUserByToken(req.query.token as string) + if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } + user.frames.post = res + let msg = req.query.msg as string + if (msg == null || msg.trim() == "") msg = "" + + user.frames.post.render(VIEWS.LAYOUT, { + page: VIEWS.WRITE_MESSAGE, + user: user, + maxlen: MAX_MESSAGE_LENGTH, + msg: msg, + placeholder: user.nextMsgPlaceholder(req) + }, (err: Error, html: string) => { user.frames.post.end(html) }) +}) +/* SUBMITTING WRITE MSG IFRAME */ +router.post(URL_PREFIX + ROUTES.WRITE_MESSAGE, (req: ZCRequest, res: Response, next: Function) => { + let user = getUserByToken(req.body.token) + if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } + user.frames.post = res + + try { + // Show the message to all users who have loaded the chatroom + broadcast(user, req.body.message, user.room, req.file) + user.frames.post.render(VIEWS.LAYOUT, { + page: VIEWS.WRITE_MESSAGE, + user: user, + maxlen: MAX_MESSAGE_LENGTH, + placeholder: user.nextMsgPlaceholder(req) + }, (err: Error, html: string) => { return user.frames.post.end(html) }) + } catch (error) { + console.error(error); + } +}) + +/* CHATROOM */ +router.post(URL_PREFIX + ROUTES.MAIN, (req: ZCRequest, res: Response, next: Function) => { + let user: typeof User = null + if (req.body.handle === undefined || req.body.passcode === undefined) { + return res.render(VIEWS.ERROR, ERRORS.INVALID_REQUEST, () => res.end) + } + user = new User(req.body.handle, req.body.passcode, res, req.body.theme, req.body.inlineView, req.body.room) + users.push(user) + + // Wait until the tripcode is done generating + user.tripcode.then((trip: string) => { + user.tripcode = trip + user.frames.chatroom.render( + VIEWS.LAYOUT, + { + page: VIEWS.CHATROOM, + user: user, + url: user.room, + writemsg: URL_PREFIX + ROUTES.WRITE_MESSAGE, + chatmsgs: URL_PREFIX + ROUTES.CHAT_MESSAGES, + settingsPanel: URL_PREFIX + ROUTES.SETTINGS, + snapbottom: true + } + ) + }) + + // Timeout and delete the user if they connect to this page but never connect to the messages iframe + user.joinTimeoutInterval = setInterval(() => { + clearInterval(user.joinTimeoutInterval) + disconnectUser(user) + }, CONNECT_DELAY_TIMEOUT) +}) + +/* SETTINGS IFRAME */ +router.get(URL_PREFIX + ROUTES.SETTINGS, (req: ZCRequest, res: Response, next: Function) => { + let user = getUserByToken(req.query.token as string) + if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } + user.frames.settings = res + + user.frames.settings.render( + VIEWS.LAYOUT, + { + page: VIEWS.SETTINGS, + user: user, + theme: user.theme, + inlineView: user.inlineView, + setSettings: req.body.setSettings, + snapbottom: true, + // redirect: URL_PREFIX + ROUTES.SETTINGS + "?token=" + req.query.token, + }, + (err: Error, html: string) => { return user.frames.settings.end(html) } + ) +}) +/* SUBMITTING SETTINGS IFRAME */ +router.post(URL_PREFIX + ROUTES.SETTINGS, (req: ZCRequest, res: Response, next: Function) => { + let user = getUserByToken(req.query.token as string) + if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } + user.frames.settings = res + + if (!req.body.setSettings) { + return res.render(VIEWS.LAYOUT, { + page: VIEWS.ERROR_MESSAGE, + url: "_hidden", + error: "Invalid request", + user: user, + redirect: URL_PREFIX + ROUTES.SETTINGS + "?token=" + req.query.token + }) + } + + if (user.theme !== req.body.theme || user.inlineView !== req.body.inlineView) { + try { + user.setTheme(req.body.theme) + user.inlineView = req.body.inlineView + user.frames.settings.render( + VIEWS.LAYOUT, + { + page: VIEWS.SETTINGS, + user: user, + theme: user.theme, + inlineView: user.inlineView, + setSettings: req.body.setSettings, + snapbottom: true, + // setSettings: true, + redirect: URL_PREFIX + ROUTES.SETTINGS + "?token=" + req.query.token, + }, + (err: Error, html: string) => { return user.frames.settings.end(html) } + ) + } catch (error) { + console.error(error) + disconnectUser(user) + } + } +}) + +/* MESSAGES IFRAME (STREAMED) */ +router.get(URL_PREFIX + ROUTES.CHAT_MESSAGES, (req: ZCRequest, res: Response, next: Function) => { + + // Find user + let user = getUserByToken(req.query.token as string) + if (!user) { return res.render(VIEWS.ERROR, ERRORS.INVALID_TOKEN) } + user.frames.chat = res + + // On disconnect, remove the user entirely + req.on("close", () => { + disconnectUser(user) + }) + + user.frames.chat.render(VIEWS.LAYOUT, { page: VIEWS.VIEW_MESSAGES, user: user }, (err: Error, html: string) => { + user.frames.chat.write(html) + user.frames.chat.render(VIEWS.NEW_MESSAGE, + { + handle: "", + tripcode: "", + messageType: "system", + message: "Users online: " + + users + .filter((iUser) => iUser.room === user.room) + .map((iUser) => iUser.handle) + .join(', '), + user: user, + timestamp: new Date().toUTCString() + }, + (err: Error, html: string) => { + user.frames.chat.write(html) + user.frames.chat.write(WHITESPACE_BITS) // Firefox needs extra data sent to render the document properly + }) + + broadcast(null, user.handle + " (" + user.tripcode + ") joined /" + user.room + ".", user.room) + + // After having loaded the messages iframe, clear out the interval + if (user.joinTimeoutInterval) + clearInterval(user.joinTimeoutInterval) + }) +}) + +module.exports = router diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71205af --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/utils/configSetup.ts b/utils/configSetup.ts new file mode 100644 index 0000000..5e14dc1 --- /dev/null +++ b/utils/configSetup.ts @@ -0,0 +1,100 @@ +const fs = require('fs') +const dotenv = require('dotenv').config() + +if (process.env.ROOMS && process.env.ROOMS.length == 0) { + throw new Error("No rooms set in .env file."); +} + +if (process.env.SECRET_SALT == "" || process.env.SECRET_SALT == null) { + console.info("INFO: You are not using a salt for passcode hashing, a randomly generated salt will be used until you set a SECRET_SALT in the .env file and restart ZeroChat."); +} +const ROOMS: String[] = (process.env.ROOMS ? process.env.ROOMS.split(",") : []) +if (ROOMS.length == 0) { + console.info("INFO: You have no rooms set for users to see publicly, none will be listed until you set a list of one or more comma separated ROOMS in the .env file and restart ZeroChat."); +} + +let configData = { + DEFAULT_THEME: process.env.DEFAULT_THEME || 'light', + DEFAULT_ROOM: (ROOMS && ROOMS[0] ? ROOMS[0] : null), + ROOMS: ROOMS, + + ROUTES: { + MAIN: '', + WRITE_MESSAGE: '_write-message', + CHAT_MESSAGES: '_view-messages', + SETTINGS: '_settings', + }, + + VIEWS: { + LAYOUT: 'layout', + FRONT_PAGE: 'index', + CHATROOM: 'chatroom', + ERROR: 'error', + WRITE_MESSAGE: 'write-message', + NEW_MESSAGE: 'new-message', + VIEW_MESSAGES: 'view-messages', + ERROR_MESSAGE: 'error-message', + SETTINGS: 'settings', + }, + + ERRORS: { + INVALID_TOKEN: { message: "Invalid Token", error: "401" }, + INVALID_REQUEST: { message: "Invalid Request", error: "400" }, + }, + + DEFAULT_INLINE_PREVIEW: process.env.DEFAULT_INLINE_PREVIEW, + + ADDRESS: process.env.ADDRESS, + HTTP_PORT: process.env.HTTP_PORT, + HTTPS_PORT: process.env.HTTPS_PORT, + USE_HTTPS: process.env.USE_HTTPS, + + HTTPS_PRIV_KEY: process.env.ENCRYPTION_HTTPS_PRIV_KEY, + HTTPS_CERT: process.env.ENCRYPTION_HTTPS_CERT, + + // Either use a secret salt or a random string + SECRET_SALT: process.env.SECRET_SALT || Math.random().toString(36).substring(16), + themes: [] as String[], + urlPrefix: '/', // not yet a variable + isValidTheme: (theme: String) => { + if (configData.themes.includes(theme)) { + return true + } else { + return false + } + }, + loadThemes: () => { + return new Promise((resolve, reject) => { + try { + fs.readdir("public/themes", (err: Error, files: Array) => { + files.forEach(file => { + if (file.endsWith(".css")) configData.themes.push(file.substr(0, file.length - 4)) + }) + + if (configData.isValidTheme(configData.DEFAULT_THEME)) { + console.log("Themes loaded: " + configData.themes); + resolve(configData.themes) + } else { + throw new Error("Default theme '" + configData.DEFAULT_THEME + "' does not exist!") + } + }) + } catch (error) { + reject(error) + } + }) + }, + loadConfig: () => { + return new Promise((resolve, reject) => { + if (configData.urlPrefix === '/') configData.urlPrefix = '' + Promise.all([configData.loadThemes()]) + .then(() => { + resolve(true) + }) + .catch((error) => { + reject(error) + }) + }) + } +} + +module.exports = configData diff --git a/utils/security.ts b/utils/security.ts new file mode 100644 index 0000000..ba1b238 --- /dev/null +++ b/utils/security.ts @@ -0,0 +1,38 @@ +const cryptography = require("crypto") +const Config = require('./configSetup') + +let chatCrypto = { + CRYPTO_ITERATIONS: 10000, + SALT: Config.SECRET_SALT, + + HASH_LENGTH: 6, + DIGEST: "sha256", + + KEY_LENGTH: 30, + TRIPCODE_LENGTH: 6, + EXPECTED_TOKEN_LENGTH: 60, + // Generate unique hash (tripcode) from password + genTripcode: (password: String) => { + return new Promise(function (resolve, reject) { + cryptography.pbkdf2( + password, + chatCrypto.SALT, + chatCrypto.CRYPTO_ITERATIONS, + chatCrypto.HASH_LENGTH, + chatCrypto.DIGEST, + (error: Error, hash: String) => { + if (error) { + reject(error) + } else { + resolve(Buffer.from(hash, 'binary').toString("base64").substr(0, chatCrypto.TRIPCODE_LENGTH)) + } + }, + ) + }) + }, + dispenseToken: () => { + return cryptography.randomBytes(chatCrypto.KEY_LENGTH).toString("hex") + } +} + +module.exports = chatCrypto diff --git a/views/chatroom.ejs b/views/chatroom.ejs index 9e671ca..8fbad8b 100644 --- a/views/chatroom.ejs +++ b/views/chatroom.ejs @@ -1,7 +1,5 @@ -
- - -
- - \ No newline at end of file + + + + diff --git a/views/error-message.ejs b/views/error-message.ejs index d4f4a83..60a3f83 100644 --- a/views/error-message.ejs +++ b/views/error-message.ejs @@ -1,4 +1,3 @@ -
<%= error %>
\ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs index e09672f..2dcf5af 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -25,9 +25,9 @@ - value="/<%= url %>" <% } %>> diff --git a/views/layout.ejs b/views/layout.ejs index 5d57931..a0faf88 100644 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -13,11 +13,15 @@ <% } else { %> <% } %> + + <% if (typeof redirect != "undefined") { %> + + <% } %> <% if (typeof url != "undefined" && !url.startsWith("_")) { %> - diff --git a/views/new-message.ejs b/views/new-message.ejs index 4696e41..289a1da 100644 --- a/views/new-message.ejs +++ b/views/new-message.ejs @@ -1,29 +1,34 @@ -
- <%= tripcode %> - <%= handle %> +
+ <%= tripcode %> + <%= handle %>
<% if (typeof file != "undefined") { %> -
+
<% if (typeof inlineView != "undefined") { %> - <% if (filetype == "image") { %> - <%= filename %> - <%= filename %> - <% } else if (filetype == "video") { %> - <%= filename %> - - <% } else if (filetype == "audio") { %> - <%= filename %> -
<% } %> - <%= message %> + <% if (typeof message != "undefined" && message != "") { %> + <%= message %> + <% } %>
-
\ No newline at end of file +
diff --git a/views/settings.ejs b/views/settings.ejs index a49c0a7..5459aee 100644 --- a/views/settings.ejs +++ b/views/settings.ejs @@ -23,7 +23,7 @@ checked <% } %> /> - +
- - +
+ - +
-
\ No newline at end of file + diff --git a/views/write-message.ejs b/views/write-message.ejs index a2925f1..b0aa8fb 100644 --- a/views/write-message.ejs +++ b/views/write-message.ejs @@ -1,9 +1,17 @@
- + - + <% if (typeof msg != "undefined") { %> + + <% } else { %> + + <% } %> +
+ +
- -
\ No newline at end of file + +