From 18377af198f394420328606fbcabd56c05923aea Mon Sep 17 00:00:00 2001 From: Mystikfluu Date: Wed, 30 Nov 2022 10:54:34 +0100 Subject: [PATCH] "small" code refactor --- extra_modules/ensureExists.js | 29 ++ extra_modules/navbar.html | 2 +- js/posts.js | 8 +- routes/api/getFileIcon.js | 45 ++ routes/api/getPosts.js | 60 +++ routes/api/post.js | 2 +- routes/api/search.js | 38 ++ routes/api/userRoutes.js | 216 +++++++++ routes/serve_static_files.js | 75 +++ routes/user_auth.js | 164 +++++++ routes/userfiles.js | 182 +++++++ server.js | 873 ++-------------------------------- 12 files changed, 858 insertions(+), 836 deletions(-) create mode 100644 extra_modules/ensureExists.js create mode 100644 routes/api/getFileIcon.js create mode 100644 routes/api/getPosts.js create mode 100644 routes/api/search.js create mode 100644 routes/api/userRoutes.js create mode 100644 routes/serve_static_files.js create mode 100644 routes/user_auth.js create mode 100644 routes/userfiles.js diff --git a/extra_modules/ensureExists.js b/extra_modules/ensureExists.js new file mode 100644 index 0000000..a1a7d82 --- /dev/null +++ b/extra_modules/ensureExists.js @@ -0,0 +1,29 @@ +import {mkdir} from "fs" + +/** + * makes sure that a given folder exists, if it doesn't it creates one for you + * @param {string} path the path of the folder + * @param {permission} mask permission mask for the new folder to have + * @param {Function} cb callback, gives null if the folder exists, otherwise gives the error + * @return {undefined} see: callback + */ + function ensureExists(path, mask, cb) { + if (typeof mask == 'function') { // Allow the `mask` parameter to be optional + cb = mask; + mask = 0o744; + } + mkdir(path, mask, function (err) { + if (err) { + if (err.code == 'EEXIST') + cb(null); // Ignore the error if the folder already exists + else + cb(err); // Something else went wrong + } + else + cb(null); // Successfully created folder + }); +} + +export { + ensureExists +} ; \ No newline at end of file diff --git a/extra_modules/navbar.html b/extra_modules/navbar.html index 60c89d0..2fe1d34 100644 --- a/extra_modules/navbar.html +++ b/extra_modules/navbar.html @@ -3,5 +3,5 @@
  • Profile
  • Posts
  • DMs
  • -
  • +
  • settings
  • \ No newline at end of file diff --git a/js/posts.js b/js/posts.js index 7f97b61..fc69e02 100644 --- a/js/posts.js +++ b/js/posts.js @@ -211,6 +211,7 @@ async function createPost(username,text,time,specialtext,postid,isbot,reply_id,a avatar.width=25; avatar.height=25; + avatar.alt = "user avatar" avatar.classList.add("avatar") if(avatar_src)avatar.src = "/avatars/"+avatar_src else { @@ -298,7 +299,12 @@ async function createPost(username,text,time,specialtext,postid,isbot,reply_id,a file2_img.width = 50 file3_img.width = 50 file4_img.width = 50 - + + file0_img.alt = "user uploaded file" + file1_img.alt = "user uploaded file" + file2_img.alt = "user uploaded file" + file3_img.alt = "user uploaded file" + file4_img.alt = "user uploaded file" if(file0){ filesP.appendChild(file0_img) diff --git a/routes/api/getFileIcon.js b/routes/api/getFileIcon.js new file mode 100644 index 0000000..6ee954b --- /dev/null +++ b/routes/api/getFileIcon.js @@ -0,0 +1,45 @@ +import sharp from "sharp" +async function addTextOnImage(text,buf) { + try { + let img = await sharp(buf) + + const metadata = await img.metadata() + + const width = metadata.width; + const height = metadata.height; + + const svgImage = ` + + + ${text} + + `; + + return await img + .composite([ + { + input: Buffer.from(svgImage), + top: 0, + left: 0, + }, + ]).webp({effort:6}).toBuffer() + } catch (error) { + console.log(error); + } +} + +export const setup = function (router, con, server) { + router.get("/api/getFileIcon/*",async function(req,res){ + let path = req.path.split("/api/getFileIcon/")[1] + if(path.length > 4) { + res.status(410).json({"error":"file ending is too long"}) + return; + } + addTextOnImage(path,await sharp("./images/empty_file.png").toBuffer()).then(buf => { + res.set("content-type","image/png") + res.send(buf) + }) + }) +} \ No newline at end of file diff --git a/routes/api/getPosts.js b/routes/api/getPosts.js new file mode 100644 index 0000000..514808f --- /dev/null +++ b/routes/api/getPosts.js @@ -0,0 +1,60 @@ +export const setup = function (router, con, server) { + router.get("/api/getPosts/*", function (_req, res) { + res.set("Access-Control-Allow-Origin", ""); + res.redirect("/api/getPosts"); + }); + router.get("/api/getPosts", function (req, res) { + res.set("Access-Control-Allow-Origin", "*"); + if (req.query.channel != undefined) { + let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,User_Avatar,file_0,file_1,file_2,file_3,file_4 from ipost.posts inner join ipost.users on (User_Name = post_user_name) where post_receiver_name = ? group by post_id order by post_id desc limit 30;`; + con.query(sql, [encodeURIComponent(req.query.channel)], function (err, result) { + if (err) + throw err; + res.json(result); + }); + } + else { //fallback + let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,file_0,file_1,file_2,file_3,file_4 from ipost.posts where (post_receiver_name is null or post_receiver_name = 'everyone') group by post_id order by post_id desc limit 30;`; + con.query(sql, [], function (err, result) { + if (err) + throw err; + res.json(result); + }); + } + }); + router.get("/api/getPostsLowerThan", function (req, res) { + res.set("Access-Control-Allow-Origin", "*"); + if (req.query.channel != undefined) { + let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,file_0,file_1,file_2,file_3,file_4 from ipost.posts where ((post_receiver_name = ?) and (post_id < ?)) group by post_id order by post_id desc limit 30;`; + con.query(sql, [encodeURIComponent(req.query.channel), req.query.id], function (err, result) { + if (err) + throw err; + res.json(result); + }); + } + else { //fallback + let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,file_0,file_1,file_2,file_3,file_4 from ipost.posts where ((post_receiver_name is null or post_receiver_name = 'everyone') and (post_id < ?)) group by post_id order by post_id desc limit 30;`; + con.query(sql, [req.query.id], function (err, result) { + if (err) + throw err; + res.json(result); + }); + } + }); + router.get("/api/getPost", function (req, res) { + res.set("Access-Control-Allow-Origin", "*"); + let arg = req.query.id; + let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,post_receiver_name,User_Avatar,file_0,file_1,file_2,file_3,file_4 from ipost.posts inner join ipost.users on (User_Name = post_user_name) where post_id=?;`; + con.query(sql, [arg], function (err, result) { + if (err) + throw err; + if (result[0]) { + res.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish + res.json(result[0]); + } + else { + res.json({ "error": "there is no such post!" }); + } + }); + }); +} \ No newline at end of file diff --git a/routes/api/post.js b/routes/api/post.js index ad1ddc8..316c67a 100644 --- a/routes/api/post.js +++ b/routes/api/post.js @@ -120,7 +120,7 @@ export const setup = function (router, con, server) { console.error(error) return; } - writeFile(`${__dirname}/user_uploads/previews/${file_name}.webp`,await sharp(file.data).resize(50,50,{fit: "inside"}).webp({mixed:true,effort:6}).toBuffer(),(error2)=>{ + writeFile(`${__dirname}/user_uploads/previews/${file_name}.webp`,await sharp(file.data).resize(100,100,{fit: "inside"}).webp({mixed:true,effort:6}).toBuffer(),(error2)=>{ if(error2)console.error(error2) }) }) diff --git a/routes/api/search.js b/routes/api/search.js new file mode 100644 index 0000000..dc683f7 --- /dev/null +++ b/routes/api/search.js @@ -0,0 +1,38 @@ +export const setup = function (router, con, server) { + router.get("/api/search", function (req, res) { + res.set("Access-Control-Allow-Origin", ""); + let type = req.query.type; + let arg = encodeURIComponent(req.query.selector); + if (type == "user") { + let sql = `select User_Name,User_Bio,User_Avatar from ipost.users where User_Name like ? limit 10;`; + con.query(sql, [`%${arg}%`], function (err, result) { + if (err) + throw err; + if (result[0] && result[0].User_Name) { + result["message"] = "search has been deprecated as of 11/30/2022" + res.json(result); + } + else { + res.json({ "error": "there is no such user!" }); + } + }); + } + else if (type == "post") { + let sql = `select post_user_name,post_text,post_time,post_special_text,post_id from ipost.posts where post_text like ? and (post_receiver_name is null or post_receiver_name = 'everyone') order by post_id desc limit 20;`; + con.query(sql, [`%${arg}%`], function (err, result) { + if (err) + throw err; + if (result[0]) { + result["message"] = "search has been deprecated as of 11/30/2022" + res.json(result); + } + else { + res.json({ "error": "there is no such post!", "message": "search has been deprecated as of 11/30/2022"}); + } + }); + } + else { + res.json({ "error": "invalid type passed along, expected `user` or `post`", "message": "search has been deprecated as of 11/30/2022"}); + } + }); +} \ No newline at end of file diff --git a/routes/api/userRoutes.js b/routes/api/userRoutes.js new file mode 100644 index 0000000..5c29119 --- /dev/null +++ b/routes/api/userRoutes.js @@ -0,0 +1,216 @@ +import sharp from "sharp" +import { ensureExists } from "../../extra_modules/ensureExists.js" +import {SHA256} from "../../extra_modules/SHA.js"; +import getIP from "../../extra_modules/getip.js"; +import {getunsigned} from "../../extra_modules/unsign.js"; + +export const setup = function (router, con, server) { + const config = server.config + const HASHES_DB = config.cookies.server_hashes; + const HASHES_COOKIE = config.cookies.client_hashes; + const HASHES_DIFF = HASHES_DB - HASHES_COOKIE; + router.post("/api/setavatar", function (req, res) { + res.set("Access-Control-Allow-Origin", ""); + if (!req.files || Object.keys(req.files).length === 0) { + return res.status(410).send('No files were uploaded. (req.files)'); + } + let avatar = req.files.avatar; + if (!avatar) { + return res.status(411).send('No files were uploaded. (req.files.)'); + } + const avatars = __dirname + '/avatars/'; + ensureExists(avatars, function (err) { + if (err) { + return res.status(500).json({ "error": "there's been an internal server error." }); + } + if (res.locals.avatar) { + try { + unlinkSync(avatars + res.locals.avatar); + } catch(ignored){} + } + let filename = genstring(95) + ".webp"; + while (existsSync(avatars + "/" + filename) || filename == ".webp") { //generate new filename until it's unique + filename = genstring(95) + ".webp"; + } + sharp(avatar.data).resize({ //resize avatar to 100x100 and convert it to a webp, then store it + width: 100, + height: 100 + }).webp({ + effort: 6, + mixed: true + }).toBuffer().then(function(data){ + writeFileSync(avatars + filename,data) + let sql = `update ipost.users set User_Avatar=? where User_Name=?`; + con.query(sql, [filename, encodeURIComponent(res.locals.username)], function (err) { + if (err) + throw err; + res.json({ "success": "updated avatar" }); + }); + }) + }); + }); + router.get("/api/getuser", function (_req, res) { + res.json({ "username": res.locals.username, "bio": res.locals.bio, "avatar": res.locals.avatar }); + }); + router.get("/api/getalluserinformation", function (req, res) { + res.set("Access-Control-Allow-Origin", ""); //we don't want that here + let unsigned = getunsigned(req, res); + if (!unsigned) + return; + unsigned = decodeURIComponent(unsigned); + let sql = `select * from ipost.users where User_Name=? and User_PW=?;`; + let values = unsigned.split(" "); + values[1] = SHA256(values[1], values[0], HASHES_DIFF); + con.query(sql, values, function (err, result) { + if (err) + throw err; + if (result[0] && result[0].User_Name && result[0].User_Name == values[0]) { + res.status(200); + res.json(result[0]); + } + else { + res.status(402); + res.json({ "error": "you cannot access the api without being logged in" }); + } + }); + }); + router.get("/api/getotheruser", function (req, res) { + res.set("Access-Control-Allow-Origin", "*"); + let username = req.query.user; + let sql = `select User_Name,User_Bio,User_Avatar from ipost.users where User_Name=?;`; + con.query(sql, [username], function (err, result) { + if (err) + throw err; + if (result[0] && result[0].User_Name && result[0].User_Name == username) { + res.json({ "username": username, "bio": result[0].User_Bio, "avatar": result[0].User_Avatar, "publicKey": result[0].User_PublicKey }); + } + else { + res.json({ "error": "there is no such user!" }); + } + }); + }); + router.post("/api/setBio", function (req, res) { + res.set("Access-Control-Allow-Origin", ""); + let bio = req.body.Bio; + if (!bio) { + res.status(410); + res.json({ "error": "no bio set!" }); + return; + } + bio = encodeURIComponent(bio); + if (bio.length > 100) { + res.status(411); + res.json({ "error": "the bio is too long!" }); + return; + } + let sql = `update ipost.users set User_Bio=? where User_Name=?`; + con.query(sql, [bio, encodeURIComponent(res.locals.username)], function (err) { + if (err) + throw err; + res.json({ "success": "updated bio" }); + }); + }); + router.post("/api/changePW", (req, res) => { + res.set("Access-Control-Allow-Origin", ""); + if ((typeof req.body.newPW) != "string") { + res.json({ "error": "incorrect password" }); + return; + } + if ((typeof req.body.currentPW) != "string") { + res.json({ "error": "incorrect password" }); + return; + } + if (req.body.newPW.length < 10) { + res.status(410); + res.json({ "error": "password is too short" }); + return; + } + let hashed_pw = SHA256(req.body.currentPW, res.locals.username, HASHES_DB); + let hashed_new_pw = SHA256(req.body.newPW, res.locals.username, HASHES_DB); + let sql = `select * from ipost.users where User_Name=? and User_PW=?;`; + let values = [res.locals.username, hashed_pw]; + con.query(sql, values, function (err, result) { + if (err) + throw err; + if (result[0] && result[0].User_Name && result[0].User_Name == res.locals.username) { + let sql = `update ipost.users set User_PW=? where User_Name=? and User_PW=?;`; + let values = [hashed_new_pw, res.locals.username, hashed_pw]; + con.query(sql, values, (err2) => { + if (err2) + throw err2; + let ip = getIP(req); + let setTo = `${res.locals.username} ${SHA256(req.body.newPW, res.locals.username, HASHES_COOKIE)}` + let cookiesigned = signature.sign(setTo, cookiesecret + ip); + res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); + res.json({ "success": "successfully changed password" }); + }); + } + else { + res.json({ "error": "invalid password" }); + } + }); + }); + router.post("/api/changeUsername", function (req, res) { + res.set("Access-Control-Allow-Origin", ""); + if ((typeof req.body.newUsername) != "string") { + res.status(410); + res.json({ "error": "incorrect username" }); + return; + } + if ((typeof req.body.currentPW) != "string") { + res.status(411); + res.json({ "error": "incorrect password" }); + return; + } + if (req.body.newUsername.length > 100) { + res.status(412); + res.json({ "error": "username is too long" }); + return; + } + if (req.body.newUsername == res.locals.username) { + res.status(413); + res.json({ "error": "username can't be the current one" }); + return; + } + let hashed_pw = SHA256(req.body.currentPW, res.locals.username, HASHES_DB); + let hashed_new_pw = SHA256(req.body.currentPW, req.body.newUsername, HASHES_DB); + let sql = `select * from ipost.users where User_Name=?;`; //check if pw is correct + let values = [res.locals.username]; + con.query(sql, values, function (err, result) { + if (err) + throw err; + if (result[0] && result[0].User_PW == hashed_pw) { + let sql = `select * from ipost.users where User_Name=?;`; //check if newUsername isn't already used + let values = [req.body.newUsername]; + con.query(sql, values, function (err, result) { + if (err) + throw err; + if (result[0]) { + res.json({ "error": "user with that username already exists" }); + return; + } + let sql = `update ipost.users set User_PW=?,User_Name=? where User_Name=? and User_PW=?;`; //change username in users + let values = [hashed_new_pw, req.body.newUsername, res.locals.username, hashed_pw]; + con.query(sql, values, function (err) { + if (err) + throw err; + let ip = getIP(req); + let setTo = `${req.body.newUsername} ${SHA256(req.body.currentPW, req.body.newUsername, HASHES_COOKIE)}` + let cookiesigned = signature.sign(setTo, cookiesecret + ip); + res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); + //updated username in the users table, but not yet on posts + //TODO: update username on dms + let sql = `update ipost.posts set post_user_name=? where post_user_name=?;`; //change username of every past post sent + let values = [req.body.newUsername, res.locals.username, hashed_pw]; + con.query(sql, values, () => { + res.json({ "success": "successfully changed username" }); //done + }); + }); + }); + } + else { + res.json({ "error": "invalid password" }); + } + }); + }); +} \ No newline at end of file diff --git a/routes/serve_static_files.js b/routes/serve_static_files.js new file mode 100644 index 0000000..4566a5e --- /dev/null +++ b/routes/serve_static_files.js @@ -0,0 +1,75 @@ +import {existsSync} from "fs" + +export const setup = function (router, con, server) { + const increaseUSERCall = server.increaseUSERCall + const __dirname = server.dirname + const dir = __dirname + "/" + + router.get("/users/*", function (req, res) { + if (!increaseUSERCall(req, res)) + return; + res.sendFile(dir + "views/otheruser.html"); + }); + router.get("/css/*", (request, response) => { + if (!increaseUSERCall(request, response)) + return; + if (existsSync(__dirname + request.originalUrl)) { + response.sendFile(__dirname + request.originalUrl); + } + else { + response.status(404).send("no file with that name found"); + } + return; + }); + router.get("/js/*", (request, response) => { + if (!increaseUSERCall(request, response)) + return; + if (existsSync(__dirname + request.originalUrl)) { + response.sendFile(__dirname + request.originalUrl); + } + else { + response.status(404).send("no file with that name found"); + } + return; + }); + router.get("/images/*", (request, response) => { + if (!increaseUSERCall(request, response)) + return; + if (existsSync(__dirname + request.originalUrl)) { + response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish + response.sendFile(__dirname + request.originalUrl); + } + else if(existsSync(__dirname + request.originalUrl.toLowerCase())){ + response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish + response.sendFile(__dirname + request.originalUrl.toLowerCase()); + } + else { + response.status(404).send("no file with that name found"); + } + return; + }); + + router.get("/user_uploads/*", (request, response) => { + if (!increaseUSERCall(request, response)) + return; + if (existsSync(__dirname + request.originalUrl)) { + response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish + response.sendFile(__dirname + request.originalUrl); + } + else { + response.status(404).send("no file with that name found"); + } + return; + }); + + router.get("/avatars/*", (request, response) => { + if (!increaseUSERCall(request, response)) + return; + response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish + let originalUrl = request.originalUrl.split("?").shift(); + if (existsSync(dir + originalUrl)) { + return response.sendFile(dir + originalUrl); + } + response.status(404).send("No avatar with that name found"); + }); +} \ No newline at end of file diff --git a/routes/user_auth.js b/routes/user_auth.js new file mode 100644 index 0000000..bead980 --- /dev/null +++ b/routes/user_auth.js @@ -0,0 +1,164 @@ +import {SHA256} from "../extra_modules/SHA.js"; +import * as signature from "cookie-signature"; +import getIP from "../extra_modules/getip.js"; +import {readFileSync} from "fs" + +const cookiesecret = readFileSync("cookiesecret.txt").toString(); + +export const setup = function (router, con, server) { + const config = server.config + const DID_I_FINALLY_ADD_HTTPS = server.DID_I_FINALLY_ADD_HTTPS + const increaseAPICall = server.increaseAPICall + const HASHES_DB = config.cookies.server_hashes; + const HASHES_COOKIE = config.cookies.client_hashes; + const HASHES_DIFF = HASHES_DB - HASHES_COOKIE; + + router.post("/register", function (req, res) { + for (let i = 0; i < 10; i++) { //don't want people spam registering + if (!increaseAPICall(req, res)) + return; + } + res.status(200); + if ((typeof req.body.user) != "string") { + res.status(416); + res.json({ "error": "incorrect username" }); + return; + } + if ((typeof req.body.pass) != "string") { + res.status(417); + res.json({ "error": "incorrect password" }); + return; + } + let username = req.body.user.toString(); + username = username.replace(/\s/gi, ""); + let password = req.body.pass.toString(); + if (!username) { + res.status(410); + res.redirect("/register?success=false&reason=username"); + return; + } + if (username == "") { + res.status(411); + res.redirect("/register?success=false&reason=username"); + return; + } + if (password.length < 10) { + res.status(412); + res.send("password is too short"); + return; + } + if (username.length > 25) { + res.status(413); + res.send("username is too long"); + return; + } + if (username.search("@") != -1) { + res.status(414); + res.send("username can't contain @-characters"); + return; + } + if (!password) { + res.status(415); + res.redirect("/register?success=false&reason=password"); + return; + } + let userexistssql = `SELECT User_Name from ipost.users where User_Name = ?`; + con.query(userexistssql, [encodeURIComponent(username)], function (_error, result) { + if (result && result[0] && result[0].User_Name) { + res.status(418); + res.redirect("/register?success=false&reason=already_exists"); + return; + } + let less_hashed_pw = SHA256(password, username, HASHES_DIFF); + let hashed_pw = SHA256(less_hashed_pw, username, HASHES_COOKIE); + let ip = getIP(req); + let setTo = `${username} ${SHA256(password, username, HASHES_COOKIE)}` + let cookiesigned = signature.sign(setTo, cookiesecret + ip); + ip = SHA256(ip, setTo, HASHES_DB); + const default_settings = {}; + let values = [encodeURIComponent(username), hashed_pw, Date.now(), ip, ip, JSON.stringify(default_settings)]; + let sql = `INSERT INTO ipost.users (User_Name, User_PW, User_CreationStamp, User_CreationIP, User_LastIP, User_Settings) VALUES (?, ?, ?, ?, ?, ?);`; + con.query(sql, values, function (err) { + if (err) + throw err; + res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); + res.redirect("/user?success=true"); + }); + }); + }); + router.post("/login", function (req, res) { + if (!increaseAPICall(req, res)) + return; + if ((typeof req.body.user) != "string") { + res.status(416); + res.json({ "error": "incorrect username" }); + return; + } + if ((typeof req.body.pass) != "string") { + res.status(417); + res.json({ "error": "incorrect password" }); + return; + } + if (!req.body.user) { + res.status(410); + res.send("no username given"); + return; + } + if (!req.body.pass) { + res.status(411); + res.send("no password given"); + return; + } + let username = req.body.user.toString(); + username = username.replace(" ", ""); + let password = req.body.pass.toString(); + if (!username) { + res.status(412); + res.send("no username given"); + return; + } + if (username.length > 25) { + res.status(413); + res.send("username is too long"); + return; + } + if (password.length < 10) { + res.status(414); + res.send("password is too short"); + return; + } + if (!password) { + res.status(415); + res.send("no password given"); + return; + } + + const no_ip_lock = username.endsWith("@unsafe") + username = username.replace("@unsafe","") + + let less_hashed_pw = SHA256(password, username, HASHES_DIFF); + let hashed_pw = SHA256(less_hashed_pw, username, HASHES_COOKIE); + let userexistssql = `SELECT * from ipost.users where User_Name = ? and User_PW = ?;`; + con.query(userexistssql, [encodeURIComponent(username), hashed_pw], function (_error, result) { + if (result && result[0]) { + let ip = getIP(req); + let setTo = `${username} ${SHA256(password, username, HASHES_COOKIE)}` + let cookiesigned = signature.sign(setTo, cookiesecret + (!no_ip_lock ? ip : "")); + res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); + ip = SHA256(ip, setTo, HASHES_DB); + if (result[0].User_LastIP != ip) { + let sql = `update ipost.users set User_LastIP = ? where User_Name = ?;`; + con.query(sql, [ip, encodeURIComponent(username)], function (error) { + if (error) + throw error; + }); + } + res.redirect("/user?success=true"); + } + else { + console.log(5,"login failed, username: ", username); + res.redirect("/login?success=false?reason=noUser"); + } + }); + }); +} \ No newline at end of file diff --git a/routes/userfiles.js b/routes/userfiles.js new file mode 100644 index 0000000..84eeab7 --- /dev/null +++ b/routes/userfiles.js @@ -0,0 +1,182 @@ +import ejs from "ejs" +import LRU from "lru-cache" +import {minify as min_js} from "uglify-js" +import Clean from 'clean-css'; +import Minifier from 'html-minifier-terser'; +import { web_version } from "unsafe_encrypt"; +import {existsSync, readFileSync, readFile} from "fs" + +export const setup = function (router, con, server) { + + const increaseUSERCall = server.increaseUSERCall + const dir = server.dirname + "/" + + ejs.cache = new LRU({max:20}) + + const load_var_cache = new LRU({ + max: 20, + maxSize: 10000, + sizeCalculation: (value) => { + return value.length + }, + ttl: 1000 * 60, + allowStale: true, + updateAgeOnGet: true, + updateAgeOnHas: true + }) + + function load_var(fina) { + if(load_var_cache.get(fina))return load_var_cache.get(fina) + if(!existsSync(fina)) { + console.log(1,"tried loading non-existent file",fina) + load_var_cache.set(fina,"") + return ""; + } + let out = readFileSync(fina) + if(fina.endsWith(".js")) { + out = min_js(out.toString()).code + } + else if(fina.endsWith(".css")) { + const { + styles, + } = new Clean({}).minify(out.toString()); + out = styles + } + + load_var_cache.set(fina,out) + + return out + } + + function get_channels(){ + return new Promise(function(resolve, reject) { + let sql = `select post_receiver_name from ipost.posts where post_is_private = '0' group by post_receiver_name;`; + con.query(sql, [], function (err, result) { + if (err)reject(err) + + let out = [] + + for(let channel of result){ + if(channel.post_receiver_name == "")continue; + out[out.length] = channel.post_receiver_name + } + + resolve(out) + }); + }) + } + + let global_page_variables = { + globalcss: load_var("./css/global.css"), + httppostjs: load_var("./js/httppost.js"), + navbar: load_var("./extra_modules/navbar.html"), + markdownjs: load_var("./js/markdown.js"), + htmlescapejs: load_var("./js/htmlescape.js"), + warnmessagejs: load_var("./js/warn_message.js"), + loadfile: load_var, + getChannels: get_channels, + encryptJS: min_js(web_version().toString()).code, + cookiebanner: ``, + newrelic: load_var("./extra_modules/newrelic_monitor.html"), + getPID: server.global_page_variables.getPID, + getDMPID: server.global_page_variables.getDMPID + } + + + + function handleUserFiles(request, response, overrideurl) { + if (!increaseUSERCall(request, response))return; + if(typeof overrideurl != "string")overrideurl = undefined; + + let originalUrl = overrideurl || request.originalUrl.split("?").shift(); + + let path = "" + if (existsSync(dir + "views" + originalUrl)) { + path = dir + "views" + originalUrl + //send .txt files as plaintext to help browsers interpret it correctly + if(originalUrl.endsWith(".txt")) { + response.set('Content-Type', 'text/plain'); + readFile(path,(err,data)=> { + if(err)return + response.send(data) + }) + return + } + } + if (existsSync(dir + "views/" + originalUrl + "index.html")) { + path = dir + "views/" + originalUrl + "index.html" + } + if (existsSync(dir + "views/" + originalUrl + ".html")) { + path = dir + "views/" + originalUrl + ".html" + } + if (existsSync(dir + "views" + originalUrl + ".html")) { + path = dir + "views" + originalUrl + ".html" + } + + if(path != "" && originalUrl != "/favicon.ico" && originalUrl != "/api/documentation/") { + global_page_variables.user = { "username": response.locals.username, "bio": response.locals.bio, "avatar": response.locals.avatar } + ejs.renderFile(path,global_page_variables,{async: true},async function(err,str){ + str = await str + err = await err + if(err) { + console.log(1,err) + response.status(500) + response.send("error") + //TODO: make error page + return + } + try { + str = await Minifier.minify(str,{ + removeComments: true, + removeCommentsFromCDATA: true, + removeCDATASectionsFromCDATA: true, + collapseWhitespace: true, + collapseBooleanAttributes: true, + removeAttributeQuotes: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true + }) + } catch(ignored){ + console.log(2,"error minifying",originalUrl); + } + + try { + response.send(str) + } catch(err) { + console.error(err) + } + }) + return; + } + + if(originalUrl == "/favicon.ico") { + response.set('Cache-Control', 'public, max-age=2592000'); + response.sendFile(dir + "/views/favicon.ico") + return + } + + if(originalUrl == "/api/documentation/") { + readFile(path,function(_err,res){ + response.send(res.toString()) + }) + return + } + + console.log(5,"no file found",originalUrl); + try { + response.status(404).send("No file with that name found"); + } catch(err) { + console.error(err) + } + } + + /** + * Handle default URI as /index (interpreted redirect: "localhost" -> "localhost/index" ) + */ + router.get("/", function (req, res) { + handleUserFiles(req,res,"/index") + }); + + router.get("/*", handleUserFiles); +} \ No newline at end of file diff --git a/server.js b/server.js index 54718ff..982e270 100644 --- a/server.js +++ b/server.js @@ -1,19 +1,16 @@ import "newrelic" import http from "http"; -import * as express from "express"; +import express,{Router} from "express"; import useragent from "express-useragent"; import fileUpload from "express-fileupload"; import * as bodyParser from "body-parser"; import cookieParser from "cookie-parser"; -import * as signature from "cookie-signature"; import * as mysql from "mysql"; import * as ws from "ws"; -import sharp from "sharp" -import {SHA256} from "./extra_modules/SHA.js"; import getIP from "./extra_modules/getip.js"; import {unsign} from "./extra_modules/unsign.js"; -import { readFileSync, mkdir, existsSync, appendFile, unlinkSync, writeFileSync, readFile } from "fs"; +import { readFileSync, appendFile } from "fs"; import { format } from "util"; import { setup as optionssetup } from "./routes/api/options.js"; import { setup as allsetup } from "./routes/api/all.js"; @@ -21,6 +18,15 @@ import { setup as settingshandlersetup } from "./routes/api/settingshandler.js"; import { setup as postsetup } from "./routes/api/post.js"; import { setup as dmsPersonalMessagessetup } from "./routes/api/dms/PersonalMessages.js"; import { setup as dmspostsetup } from "./routes/api/dms/post.js"; +import { setup as fileiconsetup } from "./routes/api/getFileIcon.js"; +import { setup as searchsetup } from "./routes/api/search.js"; +import { setup as getpostssetup } from "./routes/api/getPosts.js"; +import { setup as userroutessetup } from "./routes/api/userRoutes.js"; +import { setup as servefilessetup} from "./routes/serve_static_files.js" +import { setup as userfilessetup} from "./routes/userfiles.js" +import { setup as userauthsetup} from "./routes/user_auth.js" + +import { ensureExists } from "./extra_modules/ensureExists.js" import * as compress from "compression" const compression = compress.default @@ -30,69 +36,14 @@ import { dirname } from 'path' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) - -async function addTextOnImage(text,buf) { - try { - let img = await sharp(buf) - - const metadata = await img.metadata() - - const width = metadata.width; - const height = metadata.height; - - const svgImage = ` - - - ${text} - - `; - - return await img - .composite([ - { - input: Buffer.from(svgImage), - top: 0, - left: 0, - }, - ]).webp({effort:6}).toBuffer() - } catch (error) { - console.log(error); - } -} - -/** - * makes sure that a given folder exists, if it doesn't it creates one for you - * @param {string} path the path of the folder - * @param {permission} mask permission mask for the new folder to have - * @param {Function} cb callback, gives null if the folder exists, otherwise gives the error - * @return {undefined} see: callback - */ -function ensureExists(path, mask, cb) { - if (typeof mask == 'function') { // Allow the `mask` parameter to be optional - cb = mask; - mask = 0o744; - } - mkdir(path, mask, function (err) { - if (err) { - if (err.code == 'EEXIST') - cb(null); // Ignore the error if the folder already exists - else - cb(err); // Something else went wrong - } - else - cb(null); // Successfully created folder - }); -} const config = JSON.parse(readFileSync("server_config.json")); const time = Date.now(); const original_log = console.log; /** * custom logging function - * @param {number} level importance level if information - * @param {any} info information to format + log - * @return {undefined} returns nothing + * @param {number} level importance level if information + * @param {any} info information to format + log + * @return {undefined} returns nothing */ function log_info(level, ...info) { let text = info; @@ -121,43 +72,8 @@ console.log = log_info; const WebSocket = ws.WebSocketServer; -import {EventEmitter} from 'events'; -import merge from 'merge-descriptors'; - -/** - * Create an express application. - * - * @return {Function} - * @api public - */ - - function createApplication() { - var app = function(req, res, next) { - app.handle(req, res, next); - }; - - merge(app, EventEmitter.prototype, false); - merge(app, express.application, false); - - // expose the prototype that will get set on requests - app.request = Object.create(express.request, { - app: { configurable: true, enumerable: true, writable: true, value: app } - }) - - // expose the prototype that will get set on responses - app.response = Object.create(express.response, { - app: { configurable: true, enumerable: true, writable: true, value: app } - }) - - app.init(); - return app; -} - -const router = express.Router(); -const app = createApplication(); -const HASHES_DB = config.cookies.server_hashes; -const HASHES_COOKIE = config.cookies.client_hashes; -const HASHES_DIFF = HASHES_DB - HASHES_COOKIE; +const router = Router(); +const app = express(); const DID_I_FINALLY_ADD_HTTPS = true; const con = mysql.createPool({ connectionLimit: config.mysql.connections, @@ -165,7 +81,6 @@ const con = mysql.createPool({ user: config.mysql.user, password: readFileSync(config.mysql.password_file).toString() }); -const dir = __dirname + "/"; const cookiesecret = readFileSync("cookiesecret.txt").toString(); /** * quick function to convert data to base64 @@ -222,27 +137,11 @@ function genstring(length) { } return result; } -/** - * handles client errors, used by expressJS - */ -function clientErrorHandler(err, req, res, next) { - if (err) { - if (req.xhr) { - res.status(200).send({ error: 'Something failed!' }); - } - else { - console.log(1, err); - } - } - else { - next(); - } -} /** * utility function to get a key by its value in an object - * @param {object} object object to get key from - * @param {any} value value to get key from - * @return {any} key to the given value inside the object + * @param {object} object object to get key from + * @param {any} value value to get key from + * @return {any} key to the given value inside the object */ function getKeyByValue(object, value) { return Object.keys(object).find(key => object[key] === value); @@ -390,7 +289,6 @@ app.use(fileUpload({ app.use(bodyParser.default.json({ limit: "100mb" })); app.use(bodyParser.default.urlencoded({ limit: "100mb", extended: true })); -app.use(clientErrorHandler); app.use(cookieParser(cookiesecret)); app.use(compression()) var blocked_headers = [ @@ -450,6 +348,9 @@ app.use("/*", function (req, res, next) { next(); }); console.log(5, "finished loading user routes, starting with api routes"); + +const setuproute = handler => handler(router,con,commonfunctions) + /* START /API/* @@ -464,204 +365,23 @@ var commonfunctions = { wss, genstring, ensureExists, - "dirname": __dirname + "dirname": __dirname, + config, + DID_I_FINALLY_ADD_HTTPS }; -optionssetup(router, con, commonfunctions); -allsetup(router, con, commonfunctions); -settingshandlersetup(router, con, commonfunctions); -const get_pid = postsetup(router, con, commonfunctions); -dmsPersonalMessagessetup(router, con, commonfunctions); -const get_dmpid = dmspostsetup(router, con, commonfunctions); -router.get("/api/getFileIcon/*",async function(req,res){ - let path = req.path.split("/api/getFileIcon/")[1] - if(path.length > 4) { - res.status(410).json({"error":"file ending is too long"}) - return; - } - addTextOnImage(path,await sharp("./images/empty_file.png").toBuffer()).then(buf => { - res.set("content-type","image/png") - res.send(buf) - }) -}) +setuproute(optionssetup) +setuproute(allsetup) +setuproute(settingshandlersetup) +const get_pid = setuproute(postsetup); +setuproute(dmsPersonalMessagessetup) +const get_dmpid = setuproute(dmspostsetup); +setuproute(fileiconsetup) +setuproute(searchsetup) +setuproute(getpostssetup) +setuproute(userroutessetup) + -router.get("/api/search", function (req, res) { - res.set("Access-Control-Allow-Origin", ""); - let type = req.query.type; - let arg = encodeURIComponent(req.query.selector); - if (type == "user") { - let sql = `select User_Name,User_Bio,User_Avatar from ipost.users where User_Name like ? limit 10;`; - con.query(sql, [`%${arg}%`], function (err, result) { - if (err) - throw err; - if (result[0] && result[0].User_Name) { - res.json(result); - } - else { - res.json({ "error": "there is no such user!" }); - } - }); - } - else if (type == "post") { - let sql = `select post_user_name,post_text,post_time,post_special_text,post_id from ipost.posts where post_text like ? and (post_receiver_name is null or post_receiver_name = 'everyone') order by post_id desc limit 20;`; - con.query(sql, [`%${arg}%`], function (err, result) { - if (err) - throw err; - if (result[0]) { - res.json(result); - } - else { - res.json({ "error": "there is no such post!" }); - } - }); - } - else { - res.json({ "error": "invalid type passed along, expected `user` or `post`" }); - } -}); -router.post("/api/setavatar", function (req, res) { - res.set("Access-Control-Allow-Origin", ""); - if (!req.files || Object.keys(req.files).length === 0) { - return res.status(410).send('No files were uploaded. (req.files)'); - } - let avatar = req.files.avatar; - if (!avatar) { - return res.status(411).send('No files were uploaded. (req.files.)'); - } - let DOSbuf = Buffer.from('ffd8ffc1f151d800ff51d800ffdaffde', 'hex'); //causes DOS - if (avatar.data.includes(DOSbuf)) { - console.log(3, "DOS image was caught"); - return res.status(412).send('No files were uploaded. (req.files.)'); - } - //DOS introduced through jimp (uses jpeg-js) - const avatars = __dirname + '/avatars/'; - ensureExists(avatars, function (err) { - if (err) { - return res.status(500).json({ "error": "there's been an internal server error." }); - } - if (res.locals.avatar) { - try { - unlinkSync(avatars + res.locals.avatar); - } catch(ignored){} - } - let filename = genstring(96) + ".png"; - while (existsSync(avatars + "/" + filename) || filename == ".png") { - console.log(5, "already have file: ", filename); - original_log("already have file: ", filename); - filename = genstring(96) + ".png"; - } - sharp(avatar.data).resize({ - width: 100, - height: 100 - }).toBuffer().then(function(data){ - writeFileSync(avatars + filename,data) - let sql = `update ipost.users set User_Avatar=? where User_Name=?`; - con.query(sql, [filename, encodeURIComponent(res.locals.username)], function (err) { - if (err) - throw err; - res.json({ "success": "updated avatar" }); - }); - }) - }); -}); -router.get("/api/getuser", function (_req, res) { - res.json({ "username": res.locals.username, "bio": res.locals.bio, "avatar": res.locals.avatar }); -}); -router.get("/api/getalluserinformation", function (req, res) { - res.set("Access-Control-Allow-Origin", ""); //we don't want that here - let unsigned = unsign.getunsigned(req, res); - if (!unsigned) - return; - unsigned = decodeURIComponent(unsigned); - let sql = `select * from ipost.users where User_Name=? and User_PW=?;`; - let values = unsigned.split(" "); - values[1] = SHA256(values[1], values[0], HASHES_DIFF); - con.query(sql, values, function (err, result) { - if (err) - throw err; - if (result[0] && result[0].User_Name && result[0].User_Name == values[0]) { - res.status(200); - res.json(result[0]); - } - else { - res.status(402); - res.json({ "error": "you cannot access the api without being logged in" }); - } - }); -}); -router.get("/api/getotheruser", function (req, res) { - res.set("Access-Control-Allow-Origin", "*"); - let username = req.query.user; - let sql = `select User_Name,User_Bio,User_Avatar from ipost.users where User_Name=?;`; - con.query(sql, [username], function (err, result) { - if (err) - throw err; - if (result[0] && result[0].User_Name && result[0].User_Name == username) { - res.json({ "username": username, "bio": result[0].User_Bio, "avatar": result[0].User_Avatar, "publicKey": result[0].User_PublicKey }); - } - else { - res.json({ "error": "there is no such user!" }); - } - }); -}); -router.get("/api/getPosts/*", function (_req, res) { - res.set("Access-Control-Allow-Origin", ""); - res.redirect("/api/getPosts"); -}); -router.get("/api/getPosts", function (req, res) { - res.set("Access-Control-Allow-Origin", "*"); - if (req.query.channel != undefined) { - let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,User_Avatar,file_0,file_1,file_2,file_3,file_4 from ipost.posts inner join ipost.users on (User_Name = post_user_name) where post_receiver_name = ? group by post_id order by post_id desc limit 30;`; - con.query(sql, [encodeURIComponent(req.query.channel)], function (err, result) { - if (err) - throw err; - res.json(result); - }); - } - else { //fallback - let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,file_0,file_1,file_2,file_3,file_4 from ipost.posts where (post_receiver_name is null or post_receiver_name = 'everyone') group by post_id order by post_id desc limit 30;`; - con.query(sql, [], function (err, result) { - if (err) - throw err; - res.json(result); - }); - } -}); -router.get("/api/getPostsLowerThan", function (req, res) { - res.set("Access-Control-Allow-Origin", "*"); - if (req.query.channel != undefined) { - let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,file_0,file_1,file_2,file_3,file_4 from ipost.posts where ((post_receiver_name = ?) and (post_id < ?)) group by post_id order by post_id desc limit 30;`; - con.query(sql, [encodeURIComponent(req.query.channel), req.query.id], function (err, result) { - if (err) - throw err; - res.json(result); - }); - } - else { //fallback - let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,file_0,file_1,file_2,file_3,file_4 from ipost.posts where ((post_receiver_name is null or post_receiver_name = 'everyone') and (post_id < ?)) group by post_id order by post_id desc limit 30;`; - con.query(sql, [req.query.id], function (err, result) { - if (err) - throw err; - res.json(result); - }); - } -}); -router.get("/api/getPost", function (req, res) { - res.set("Access-Control-Allow-Origin", "*"); - let arg = req.query.id; - let sql = `select post_user_name,post_text,post_time,post_special_text,post_id,post_from_bot,post_reply_id,post_receiver_name,User_Avatar,file_0,file_1,file_2,file_3,file_4 from ipost.posts inner join ipost.users on (User_Name = post_user_name) where post_id=?;`; - con.query(sql, [arg], function (err, result) { - if (err) - throw err; - if (result[0]) { - res.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish - res.json(result[0]); - } - else { - res.json({ "error": "there is no such post!" }); - } - }); -}); router.get("/api/getChannels", function (_req, res) { res.set("Access-Control-Allow-Origin", "*"); let sql = `select post_receiver_name from ipost.posts where post_is_private = '0' group by post_receiver_name;`; @@ -671,541 +391,28 @@ router.get("/api/getChannels", function (_req, res) { res.json(result); }); }); -router.post("/api/setBio", function (req, res) { - res.set("Access-Control-Allow-Origin", ""); - let bio = req.body.Bio; - if (!bio) { - res.status(410); - res.json({ "error": "no bio set!" }); - return; - } - bio = encodeURIComponent(bio); - if (bio.length > 100) { - res.status(411); - res.json({ "error": "the bio is too long!" }); - return; - } - let sql = `update ipost.users set User_Bio=? where User_Name=?`; - con.query(sql, [bio, encodeURIComponent(res.locals.username)], function (err) { - if (err) - throw err; - res.json({ "success": "updated bio" }); - }); -}); -router.post("/api/changePW", (req, res) => { - res.set("Access-Control-Allow-Origin", ""); - if ((typeof req.body.newPW) != "string") { - res.json({ "error": "incorrect password" }); - return; - } - if ((typeof req.body.currentPW) != "string") { - res.json({ "error": "incorrect password" }); - return; - } - if (req.body.newPW.length < 10) { - res.status(410); - res.json({ "error": "password is too short" }); - return; - } - let hashed_pw = SHA256(req.body.currentPW, res.locals.username, HASHES_DB); - let hashed_new_pw = SHA256(req.body.newPW, res.locals.username, HASHES_DB); - let sql = `select * from ipost.users where User_Name=? and User_PW=?;`; - let values = [res.locals.username, hashed_pw]; - con.query(sql, values, function (err, result) { - if (err) - throw err; - if (result[0] && result[0].User_Name && result[0].User_Name == res.locals.username) { - let sql = `update ipost.users set User_PW=? where User_Name=? and User_PW=?;`; - let values = [hashed_new_pw, res.locals.username, hashed_pw]; - con.query(sql, values, (err2) => { - if (err2) - throw err2; - let ip = getIP(req); - let setTo = `${res.locals.username} ${SHA256(req.body.newPW, res.locals.username, HASHES_COOKIE)}` - let cookiesigned = signature.sign(setTo, cookiesecret + ip); - res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); - res.json({ "success": "successfully changed password" }); - }); - } - else { - res.json({ "error": "invalid password" }); - } - }); -}); -router.post("/api/changeUsername", function (req, res) { - res.set("Access-Control-Allow-Origin", ""); - if ((typeof req.body.newUsername) != "string") { - res.status(410); - res.json({ "error": "incorrect username" }); - return; - } - if ((typeof req.body.currentPW) != "string") { - res.status(411); - res.json({ "error": "incorrect password" }); - return; - } - if (req.body.newUsername.length > 100) { - res.status(412); - res.json({ "error": "username is too long" }); - return; - } - if (req.body.newUsername == res.locals.username) { - res.status(413); - res.json({ "error": "username can't be the current one" }); - return; - } - let hashed_pw = SHA256(req.body.currentPW, res.locals.username, HASHES_DB); - let hashed_new_pw = SHA256(req.body.currentPW, req.body.newUsername, HASHES_DB); - let sql = `select * from ipost.users where User_Name=?;`; //check if pw is correct - let values = [res.locals.username]; - con.query(sql, values, function (err, result) { - if (err) - throw err; - if (result[0] && result[0].User_PW == hashed_pw) { - let sql = `select * from ipost.users where User_Name=?;`; //check if newUsername isn't already used - let values = [req.body.newUsername]; - con.query(sql, values, function (err, result) { - if (err) - throw err; - if (result[0]) { - res.json({ "error": "user with that username already exists" }); - return; - } - let sql = `update ipost.users set User_PW=?,User_Name=? where User_Name=? and User_PW=?;`; //change username in users - let values = [hashed_new_pw, req.body.newUsername, res.locals.username, hashed_pw]; - con.query(sql, values, function (err) { - if (err) - throw err; - let ip = getIP(req); - let setTo = `${req.body.newUsername} ${SHA256(req.body.currentPW, req.body.newUsername, HASHES_COOKIE)}` - let cookiesigned = signature.sign(setTo, cookiesecret + ip); - res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); - //updated username in the users table, but not yet on posts - let sql = `update ipost.posts set post_user_name=? where post_user_name=?;`; //change username of every past post sent - let values = [req.body.newUsername, res.locals.username, hashed_pw]; - con.query(sql, values, () => { - res.json({ "success": "successfully changed username" }); //done - }); - }); - }); - } - else { - res.json({ "error": "invalid password" }); - } - }); -}); /* END /API/* */ -router.get("/users/*", function (req, res) { - if (!increaseUSERCall(req, res)) - return; - res.sendFile(dir + "views/otheruser.html"); -}); -router.get("/css/*", (request, response) => { - if (!increaseUSERCall(request, response)) - return; - if (existsSync(__dirname + request.originalUrl)) { - response.sendFile(__dirname + request.originalUrl); - } - else { - response.status(404).send("no file with that name found"); - } - return; -}); -router.get("/js/*", (request, response) => { - if (!increaseUSERCall(request, response)) - return; - if (existsSync(__dirname + request.originalUrl)) { - response.sendFile(__dirname + request.originalUrl); - } - else { - response.status(404).send("no file with that name found"); - } - return; -}); -router.get("/images/*", (request, response) => { - if (!increaseUSERCall(request, response)) - return; - if (existsSync(__dirname + request.originalUrl)) { - response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish - response.sendFile(__dirname + request.originalUrl); - } - else if(existsSync(__dirname + request.originalUrl.toLowerCase())){ - response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish - response.sendFile(__dirname + request.originalUrl.toLowerCase()); - } - else { - response.status(404).send("no file with that name found"); - } - return; -}); -router.get("/user_uploads/*", (request, response) => { - if (!increaseUSERCall(request, response)) - return; - if (existsSync(__dirname + request.originalUrl)) { - response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish - response.sendFile(__dirname + request.originalUrl); - } - else { - response.status(404).send("no file with that name found"); - } - return; -}); +setuproute(servefilessetup) -router.get("/avatars/*", (request, response) => { - if (!increaseUSERCall(request, response)) - return; - response.set('Cache-Control', 'public, max-age=2592000'); //cache it for one month-ish - let originalUrl = request.originalUrl.split("?").shift(); - if (existsSync(dir + originalUrl + ".png")) { - return response.sendFile(dir + originalUrl + ".png"); - } - if (existsSync(dir + originalUrl)) { - return response.sendFile(dir + originalUrl); - } - response.status(404).send("No avatar with that name found"); -}); router.get("/logout", function (_req, res) { res.cookie("AUTH_COOKIE", "", { maxAge: 0, httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); res.redirect("/"); }); -import ejs from "ejs" -import LRU from "lru-cache" - -ejs.cache = new LRU({max:20}) - -const load_var_cache = new LRU({ - max: 20, - maxSize: 10000, - sizeCalculation: (value) => { - return value.length - }, - ttl: 1000 * 60, - allowStale: true, - updateAgeOnGet: true, - updateAgeOnHas: true -}) - -import {minify as min_js} from "uglify-js" -import Clean from 'clean-css'; -import Minifier from 'html-minifier-terser'; - -function load_var(fina) { - if(load_var_cache.get(fina))return load_var_cache.get(fina) - if(!existsSync(fina)) { - console.log(1,"tried loading non-existent file",fina) - load_var_cache.set(fina,"") - return ""; - } - let out = readFileSync(fina) - if(fina.endsWith(".js")) { - out = min_js(out.toString()).code - } - else if(fina.endsWith(".css")) { - const { - styles, - } = new Clean({}).minify(out.toString()); - out = styles - } - - load_var_cache.set(fina,out) - - return out -} - -function get_channels(){ - return new Promise(function(resolve, reject) { - let sql = `select post_receiver_name from ipost.posts where post_is_private = '0' group by post_receiver_name;`; - con.query(sql, [], function (err, result) { - if (err)reject(err) - - let out = [] - - for(let channel of result){ - if(channel.post_receiver_name == "")continue; - out[out.length] = channel.post_receiver_name - } - - resolve(out) - }); - }) -} - -import { web_version } from "unsafe_encrypt"; - let global_page_variables = { - globalcss: load_var("./css/global.css"), - httppostjs: load_var("./js/httppost.js"), - navbar: load_var("./extra_modules/navbar.html"), - markdownjs: load_var("./js/markdown.js"), - htmlescapejs: load_var("./js/htmlescape.js"), - warnmessagejs: load_var("./js/warn_message.js"), - loadfile: load_var, - getChannels: get_channels, getPID: get_pid, getDMPID: get_dmpid, - encryptJS: min_js(web_version().toString()).code, - cookiebanner: ``, - newrelic: load_var("./extra_modules/newrelic_monitor.html") } +commonfunctions.global_page_variables = global_page_variables +setuproute(userfilessetup) //needs getPID and getDMPID - function handleUserFiles(request, response, overrideurl) { - if (!increaseUSERCall(request, response))return; - if(typeof overrideurl != "string")overrideurl = undefined; +setuproute(userauthsetup) //login & register - let originalUrl = overrideurl || request.originalUrl.split("?").shift(); - - let path = "" - //console.log("handling user file") - if (existsSync(dir + "views" + originalUrl)) { - //console.log("exists without additional path"); - path = dir + "views" + originalUrl - if(originalUrl.endsWith(".txt")) { - response.set('Content-Type', 'text/plain'); - //console.log("sending txt file") - readFile(path,(err,data)=> { - if(err)return - response.send(data) - }) - return - } - //return response.sendFile(dir + "views" + originalUrl); - } - if (existsSync(dir + "views/" + originalUrl + "index.html")) { - path = dir + "views/" + originalUrl + "index.html" - } - if (existsSync(dir + "views/" + originalUrl + ".html")) { - path = dir + "views/" + originalUrl + ".html" - //return response.sendFile(dir + "views/" + originalUrl + ".html"); - } - if (existsSync(dir + "views" + originalUrl + ".html")) { - path = dir + "views" + originalUrl + ".html" - //return response.sendFile(dir + "views" + originalUrl + ".html"); - } - - if(path != "" && originalUrl != "/favicon.ico" && originalUrl != "/api/documentation/") { - global_page_variables.user = { "username": response.locals.username, "bio": response.locals.bio, "avatar": response.locals.avatar } - ejs.renderFile(path,global_page_variables,{async: true},async function(err,str){ - str = await str - err = await err - if(err) { - console.log(1,err) - response.status(500) - response.send("error") - //TODO: make error page - return - } - try { - str = await Minifier.minify(str,{ - removeComments: true, - removeCommentsFromCDATA: true, - removeCDATASectionsFromCDATA: true, - collapseWhitespace: true, - collapseBooleanAttributes: true, - removeAttributeQuotes: true, - removeRedundantAttributes: true, - useShortDoctype: true, - removeEmptyAttributes: true - }) - } catch(ignored){ - console.log(2,"error minifying",originalUrl); - } - - try { - response.send(str) - } catch(err) { - console.error(err) - } - }) - return; - } - - if(originalUrl == "/favicon.ico") { - response.set('Cache-Control', 'public, max-age=2592000'); - response.sendFile(dir + "/views/favicon.ico") - return - } - - if(originalUrl == "/api/documentation/") { - readFile(path,function(_err,res){ - response.send(res.toString()) - }) - return - } - - console.log(5,"no file found",originalUrl); - try { - response.status(404).send("No file with that name found"); - } catch(err) { - console.error(err) - } -} - -router.get("/", function (req, res) { - // if (!increaseUSERCall(req, res)) - // return; - handleUserFiles(req,res,"/index") - //res.sendFile(dir + "views/index.html"); -}); - -router.get("/*", handleUserFiles); - -router.post("/register", function (req, res) { - for (let i = 0; i < 10; i++) { //don't want people spam registering - if (!increaseAPICall(req, res)) - return; - } - res.status(200); - if ((typeof req.body.user) != "string") { - res.status(416); - res.json({ "error": "incorrect username" }); - return; - } - if ((typeof req.body.pass) != "string") { - res.status(417); - res.json({ "error": "incorrect password" }); - return; - } - let username = req.body.user.toString(); - username = username.replace(/\s/gi, ""); - let password = req.body.pass.toString(); - if (!username) { - res.status(410); - res.redirect("/register?success=false&reason=username"); - return; - } - if (username == "") { - res.status(411); - res.redirect("/register?success=false&reason=username"); - return; - } - if (password.length < 10) { - res.status(412); - res.send("password is too short"); - return; - } - if (username.length > 25) { - res.status(413); - res.send("username is too long"); - return; - } - if (username.search("@") != -1) { - res.status(414); - res.send("username can't contain @-characters"); - return; - } - if (!password) { - res.status(415); - res.redirect("/register?success=false&reason=password"); - return; - } - let userexistssql = `SELECT User_Name from ipost.users where User_Name = ?`; - con.query(userexistssql, [encodeURIComponent(username)], function (_error, result) { - if (result && result[0] && result[0].User_Name) { - res.status(418); - res.redirect("/register?success=false&reason=already_exists"); - return; - } - let less_hashed_pw = SHA256(password, username, HASHES_DIFF); - let hashed_pw = SHA256(less_hashed_pw, username, HASHES_COOKIE); - let ip = getIP(req); - let setTo = `${username} ${SHA256(password, username, HASHES_COOKIE)}` - let cookiesigned = signature.sign(setTo, cookiesecret + ip); - ip = SHA256(ip, setTo, HASHES_DB); - const default_settings = {}; - let values = [encodeURIComponent(username), hashed_pw, Date.now(), ip, ip, JSON.stringify(default_settings)]; - let sql = `INSERT INTO ipost.users (User_Name, User_PW, User_CreationStamp, User_CreationIP, User_LastIP, User_Settings) VALUES (?, ?, ?, ?, ?, ?);`; - con.query(sql, values, function (err) { - if (err) - throw err; - res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); - res.redirect("/user?success=true"); - }); - }); -}); -router.post("/login", function (req, res) { - if (!increaseAPICall(req, res)) - return; - if (!increaseAPICall(req, res)) - return; - //login is counted twice (think of bruteforces man) - if ((typeof req.body.user) != "string") { - res.status(416); - res.json({ "error": "incorrect username" }); - return; - } - if ((typeof req.body.pass) != "string") { - res.status(417); - res.json({ "error": "incorrect password" }); - return; - } - if (!req.body.user) { - res.status(410); - res.send("no username given"); - return; - } - if (!req.body.pass) { - res.status(411); - res.send("no password given"); - return; - } - let username = req.body.user.toString(); - username = username.replace(" ", ""); - let password = req.body.pass.toString(); - if (!username) { - res.status(412); - res.send("no username given"); - return; - } - if (username.length > 25) { - res.status(413); - res.send("username is too long"); - return; - } - if (password.length < 10) { - res.status(414); - res.send("password is too short"); - return; - } - if (!password) { - res.status(415); - res.send("no password given"); - return; - } - - const no_ip_lock = username.endsWith("@unsafe") - username = username.replace("@unsafe","") - - let less_hashed_pw = SHA256(password, username, HASHES_DIFF); - let hashed_pw = SHA256(less_hashed_pw, username, HASHES_COOKIE); - let userexistssql = `SELECT * from ipost.users where User_Name = ? and User_PW = ?;`; - con.query(userexistssql, [encodeURIComponent(username), hashed_pw], function (_error, result) { - if (result && result[0]) { - let ip = getIP(req); - let setTo = `${username} ${SHA256(password, username, HASHES_COOKIE)}` - let cookiesigned = signature.sign(setTo, cookiesecret + (!no_ip_lock ? ip : "")); - res.cookie('AUTH_COOKIE', cookiesigned, { maxAge: Math.pow(10, 10), httpOnly: true, secure: DID_I_FINALLY_ADD_HTTPS }); - ip = SHA256(ip, setTo, HASHES_DB); - if (result[0].User_LastIP != ip) { - let sql = `update ipost.users set User_LastIP = ? where User_Name = ?;`; - con.query(sql, [ip, encodeURIComponent(username)], function (error) { - if (error) - throw error; - }); - } - res.redirect("/user?success=true"); - } - else { - console.log(5,"login failed, username: ", username); - res.redirect("/login?success=false?reason=noUser"); - } - }); -}); console.log(5, "finished loading routes"); app.use(router); const httpServer = http.createServer(app);