main-socket-handler.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import { SocketHandler } from "../socket-handler.js";
  2. import { DockgeServer } from "../dockge-server";
  3. import { log } from "../log";
  4. import { R } from "redbean-node";
  5. import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
  6. import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
  7. import { User } from "../models/user";
  8. import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server";
  9. import { passwordStrength } from "check-password-strength";
  10. import jwt from "jsonwebtoken";
  11. import { Settings } from "../settings";
  12. export class MainSocketHandler extends SocketHandler {
  13. create(socket : DockgeSocket, server : DockgeServer) {
  14. // ***************************
  15. // Public Socket API
  16. // ***************************
  17. // Setup
  18. socket.on("setup", async (username, password, callback) => {
  19. try {
  20. if (passwordStrength(password).value === "Too weak") {
  21. throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
  22. }
  23. if ((await R.knex("user").count("id as count").first()).count !== 0) {
  24. throw new Error("Dockge has been initialized. If you want to run setup again, please delete the database.");
  25. }
  26. const user = R.dispense("user");
  27. user.username = username;
  28. user.password = generatePasswordHash(password);
  29. await R.store(user);
  30. server.needSetup = false;
  31. callback({
  32. ok: true,
  33. msg: "successAdded",
  34. msgi18n: true,
  35. });
  36. } catch (e) {
  37. if (e instanceof Error) {
  38. callback({
  39. ok: false,
  40. msg: e.message,
  41. });
  42. }
  43. }
  44. });
  45. // Login by token
  46. socket.on("loginByToken", async (token, callback) => {
  47. const clientIP = await server.getClientIP(socket);
  48. log.info("auth", `Login by token. IP=${clientIP}`);
  49. try {
  50. const decoded = jwt.verify(token, server.jwtSecret) as JWTDecoded;
  51. log.info("auth", "Username from JWT: " + decoded.username);
  52. const user = await R.findOne("user", " username = ? AND active = 1 ", [
  53. decoded.username,
  54. ]) as User;
  55. if (user) {
  56. // Check if the password changed
  57. if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
  58. throw new Error("The token is invalid due to password change or old token");
  59. }
  60. log.debug("auth", "afterLogin");
  61. await server.afterLogin(socket, user);
  62. log.debug("auth", "afterLogin ok");
  63. log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
  64. callback({
  65. ok: true,
  66. });
  67. } else {
  68. log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
  69. callback({
  70. ok: false,
  71. msg: "authUserInactiveOrDeleted",
  72. msgi18n: true,
  73. });
  74. }
  75. } catch (error) {
  76. if (!(error instanceof Error)) {
  77. console.error("Unknown error:", error);
  78. return;
  79. }
  80. log.error("auth", `Invalid token. IP=${clientIP}`);
  81. if (error.message) {
  82. log.error("auth", error.message + ` IP=${clientIP}`);
  83. }
  84. callback({
  85. ok: false,
  86. msg: "authInvalidToken",
  87. msgi18n: true,
  88. });
  89. }
  90. });
  91. // Login
  92. socket.on("login", async (data, callback) => {
  93. const clientIP = await server.getClientIP(socket);
  94. log.info("auth", `Login by username + password. IP=${clientIP}`);
  95. // Checking
  96. if (typeof callback !== "function") {
  97. return;
  98. }
  99. if (!data) {
  100. return;
  101. }
  102. // Login Rate Limit
  103. if (!await loginRateLimiter.pass(callback)) {
  104. log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
  105. return;
  106. }
  107. const user = await this.login(data.username, data.password);
  108. if (user) {
  109. if (user.twofa_status === 0) {
  110. server.afterLogin(socket, user);
  111. log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
  112. callback({
  113. ok: true,
  114. token: User.createJWT(user, server.jwtSecret),
  115. });
  116. }
  117. if (user.twofa_status === 1 && !data.token) {
  118. log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
  119. callback({
  120. tokenRequired: true,
  121. });
  122. }
  123. if (data.token) {
  124. // @ts-ignore
  125. const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
  126. if (user.twofa_last_token !== data.token && verify) {
  127. server.afterLogin(socket, user);
  128. await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
  129. data.token,
  130. socket.userID,
  131. ]);
  132. log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
  133. callback({
  134. ok: true,
  135. token: User.createJWT(user, server.jwtSecret),
  136. });
  137. } else {
  138. log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
  139. callback({
  140. ok: false,
  141. msg: "authInvalidToken",
  142. msgi18n: true,
  143. });
  144. }
  145. }
  146. } else {
  147. log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
  148. callback({
  149. ok: false,
  150. msg: "authIncorrectCreds",
  151. msgi18n: true,
  152. });
  153. }
  154. });
  155. // Change Password
  156. socket.on("changePassword", async (password, callback) => {
  157. try {
  158. checkLogin(socket);
  159. if (! password.newPassword) {
  160. throw new Error("Invalid new password");
  161. }
  162. if (passwordStrength(password.newPassword).value === "Too weak") {
  163. throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
  164. }
  165. let user = await doubleCheckPassword(socket, password.currentPassword);
  166. await user.resetPassword(password.newPassword);
  167. callback({
  168. ok: true,
  169. msg: "Password has been updated successfully.",
  170. });
  171. } catch (e) {
  172. if (e instanceof Error) {
  173. callback({
  174. ok: false,
  175. msg: e.message,
  176. });
  177. }
  178. }
  179. });
  180. socket.on("getSettings", async (callback) => {
  181. try {
  182. checkLogin(socket);
  183. const data = await Settings.getSettings("general");
  184. callback({
  185. ok: true,
  186. data: data,
  187. });
  188. } catch (e) {
  189. if (e instanceof Error) {
  190. callback({
  191. ok: false,
  192. msg: e.message,
  193. });
  194. }
  195. }
  196. });
  197. socket.on("setSettings", async (data, currentPassword, callback) => {
  198. try {
  199. checkLogin(socket);
  200. // If currently is disabled auth, don't need to check
  201. // Disabled Auth + Want to Disable Auth => No Check
  202. // Disabled Auth + Want to Enable Auth => No Check
  203. // Enabled Auth + Want to Disable Auth => Check!!
  204. // Enabled Auth + Want to Enable Auth => No Check
  205. const currentDisabledAuth = await Settings.get("disableAuth");
  206. if (!currentDisabledAuth && data.disableAuth) {
  207. await doubleCheckPassword(socket, currentPassword);
  208. }
  209. console.log(data);
  210. await Settings.setSettings("general", data);
  211. callback({
  212. ok: true,
  213. msg: "Saved"
  214. });
  215. server.sendInfo(socket);
  216. } catch (e) {
  217. if (e instanceof Error) {
  218. callback({
  219. ok: false,
  220. msg: e.message,
  221. });
  222. }
  223. }
  224. });
  225. }
  226. async login(username : string, password : string) : Promise<User | null> {
  227. if (typeof username !== "string" || typeof password !== "string") {
  228. return null;
  229. }
  230. const user = await R.findOne("user", " username = ? AND active = 1 ", [
  231. username,
  232. ]) as User;
  233. if (user && verifyPassword(password, user.password)) {
  234. // Upgrade the hash to bcrypt
  235. if (needRehashPassword(user.password)) {
  236. await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
  237. generatePasswordHash(password),
  238. user.id,
  239. ]);
  240. }
  241. return user;
  242. }
  243. return null;
  244. }
  245. }