import { SocketHandler } from "../socket-handler.js"; import { Socket } from "socket.io"; import { DockgeServer } from "../dockge-server"; import { log } from "../log"; import { R } from "redbean-node"; import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter"; import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash"; import { User } from "../models/user"; import { checkLogin, DockgeSocket, doubleCheckPassword } from "../util-server"; import { passwordStrength } from "check-password-strength"; import jwt from "jsonwebtoken"; import { Settings } from "../settings"; export class MainSocketHandler extends SocketHandler { create(socket : DockgeSocket, server : DockgeServer) { // *************************** // Public Socket API // *************************** // Setup socket.on("setup", async (username, password, callback) => { try { if (passwordStrength(password).value === "Too weak") { throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); } if ((await R.knex("user").count("id as count").first()).count !== 0) { throw new Error("Dockge has been initialized. If you want to run setup again, please delete the database."); } const user = R.dispense("user"); user.username = username; user.password = generatePasswordHash(password); await R.store(user); server.needSetup = false; callback({ ok: true, msg: "successAdded", msgi18n: true, }); } catch (e) { callback({ ok: false, msg: e.message, }); } }); // Login by token socket.on("loginByToken", async (token, callback) => { const clientIP = await server.getClientIP(socket); log.info("auth", `Login by token. IP=${clientIP}`); try { const decoded = jwt.verify(token, server.jwtSecret); log.info("auth", "Username from JWT: " + decoded.username); const user = await R.findOne("user", " username = ? AND active = 1 ", [ decoded.username, ]) as User; if (user) { // Check if the password changed if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) { throw new Error("The token is invalid due to password change or old token"); } log.debug("auth", "afterLogin"); await this.afterLogin(server, socket, user); log.debug("auth", "afterLogin ok"); log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`); callback({ ok: true, }); } else { log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`); callback({ ok: false, msg: "authUserInactiveOrDeleted", msgi18n: true, }); } } catch (error) { log.error("auth", `Invalid token. IP=${clientIP}`); if (error.message) { log.error("auth", error.message, `IP=${clientIP}`); } callback({ ok: false, msg: "authInvalidToken", msgi18n: true, }); } }); // Login socket.on("login", async (data, callback) => { const clientIP = await server.getClientIP(socket); log.info("auth", `Login by username + password. IP=${clientIP}`); // Checking if (typeof callback !== "function") { return; } if (!data) { return; } // Login Rate Limit if (!await loginRateLimiter.pass(callback)) { log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`); return; } const user = await this.login(data.username, data.password); if (user) { if (user.twofa_status === 0) { this.afterLogin(server, socket, user); log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`); callback({ ok: true, token: User.createJWT(user, server.jwtSecret), }); } if (user.twofa_status === 1 && !data.token) { log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`); callback({ tokenRequired: true, }); } if (data.token) { const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions); if (user.twofa_last_token !== data.token && verify) { this.afterLogin(server, socket, user); await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [ data.token, socket.userID, ]); log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`); callback({ ok: true, token: User.createJWT(user, server.jwtSecret), }); } else { log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`); callback({ ok: false, msg: "authInvalidToken", msgi18n: true, }); } } } else { log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`); callback({ ok: false, msg: "authIncorrectCreds", msgi18n: true, }); } }); socket.on("getSettings", async (callback) => { try { checkLogin(socket); const data = await Settings.getSettings("general"); callback({ ok: true, data: data, }); } catch (e) { callback({ ok: false, msg: e.message, }); } }); socket.on("setSettings", async (data, currentPassword, callback) => { try { checkLogin(socket); // If currently is disabled auth, don't need to check // Disabled Auth + Want to Disable Auth => No Check // Disabled Auth + Want to Enable Auth => No Check // Enabled Auth + Want to Disable Auth => Check!! // Enabled Auth + Want to Enable Auth => No Check const currentDisabledAuth = await Settings.get("disableAuth"); if (!currentDisabledAuth && data.disableAuth) { await doubleCheckPassword(socket, currentPassword); } await Settings.setSettings("general", data); callback({ ok: true, msg: "Saved" }); server.sendInfo(socket); } catch (e) { callback({ ok: false, msg: e.message, }); } }); } async afterLogin(server: DockgeServer, socket : DockgeSocket, user : User) { socket.userID = user.id; socket.join(user.id.toString()); server.sendInfo(socket); try { server.sendStackList(socket); } catch (e) { log.error("server", e); } } async login(username : string, password : string) { if (typeof username !== "string" || typeof password !== "string") { return null; } const user = await R.findOne("user", " username = ? AND active = 1 ", [ username, ]); if (user && verifyPassword(password, user.password)) { // Upgrade the hash to bcrypt if (needRehashPassword(user.password)) { await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ generatePasswordHash(password), user.id, ]); } return user; } return null; } }