mirror of
https://github.com/louislam/dockge.git
synced 2024-11-23 19:34:04 +00:00
347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
// @ts-ignore
|
|
import composerize from "composerize";
|
|
import { SocketHandler } from "../socket-handler.js";
|
|
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 {
|
|
callbackError,
|
|
checkLogin,
|
|
DockgeSocket,
|
|
doubleCheckPassword,
|
|
JWTDecoded,
|
|
ValidationError
|
|
} 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) {
|
|
if (e instanceof Error) {
|
|
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) as JWTDecoded;
|
|
|
|
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 server.afterLogin(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) {
|
|
if (!(error instanceof Error)) {
|
|
console.error("Unknown error:", error);
|
|
return;
|
|
}
|
|
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) {
|
|
server.afterLogin(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) {
|
|
// @ts-ignore
|
|
const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
|
|
|
|
if (user.twofa_last_token !== data.token && verify) {
|
|
server.afterLogin(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,
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
// Change Password
|
|
socket.on("changePassword", async (password, callback) => {
|
|
try {
|
|
checkLogin(socket);
|
|
|
|
if (! password.newPassword) {
|
|
throw new Error("Invalid new password");
|
|
}
|
|
|
|
if (passwordStrength(password.newPassword).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.");
|
|
}
|
|
|
|
let user = await doubleCheckPassword(socket, password.currentPassword);
|
|
await user.resetPassword(password.newPassword);
|
|
|
|
server.disconnectAllSocketClients(user.id, socket.id);
|
|
|
|
callback({
|
|
ok: true,
|
|
msg: "Password has been updated successfully.",
|
|
});
|
|
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
callback({
|
|
ok: false,
|
|
msg: e.message,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on("getSettings", async (callback) => {
|
|
try {
|
|
checkLogin(socket);
|
|
const data = await Settings.getSettings("general");
|
|
|
|
callback({
|
|
ok: true,
|
|
data: data,
|
|
});
|
|
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
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) {
|
|
if (e instanceof Error) {
|
|
callback({
|
|
ok: false,
|
|
msg: e.message,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Disconnect all other socket clients of the user
|
|
socket.on("disconnectOtherSocketClients", async () => {
|
|
try {
|
|
checkLogin(socket);
|
|
server.disconnectAllSocketClients(socket.userID, socket.id);
|
|
} catch (e) {
|
|
if (e instanceof Error) {
|
|
log.warn("disconnectOtherSocketClients", e.message);
|
|
}
|
|
}
|
|
});
|
|
|
|
// composerize
|
|
socket.on("composerize", async (dockerRunCommand : unknown, callback) => {
|
|
try {
|
|
checkLogin(socket);
|
|
|
|
if (typeof(dockerRunCommand) !== "string") {
|
|
throw new ValidationError("dockerRunCommand must be a string");
|
|
}
|
|
|
|
const composeTemplate = composerize(dockerRunCommand);
|
|
callback({
|
|
ok: true,
|
|
composeTemplate,
|
|
});
|
|
} catch (e) {
|
|
callbackError(e, callback);
|
|
}
|
|
});
|
|
}
|
|
|
|
async login(username : string, password : string) : Promise<User | null> {
|
|
if (typeof username !== "string" || typeof password !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const user = await R.findOne("user", " username = ? AND active = 1 ", [
|
|
username,
|
|
]) as User;
|
|
|
|
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;
|
|
}
|
|
}
|