From 6ae5a177750dd634d466ff98d8e62927e585aae7 Mon Sep 17 00:00:00 2001 From: jpratham Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 01/13] added code to use block ip mechanism in cli --- api/auth.ts | 12 ++++++++---- pages/login/index.tsx | 10 +++++++--- services/auth.service.ts | 7 ++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index c7e7e7d..36f33cf 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -1,4 +1,4 @@ -import express, { Request, Response, NextFunction } from 'express' +import express, { Request, Response, NextFunction, response } from 'express' import { execFile } from 'child_process' import { cliStderrResponse, unautorizedResponse } from './handlers/util' import * as crypto from '@shardus/crypto-utils'; @@ -19,11 +19,12 @@ const jwtSecret = (isValidSecret(process.env.JWT_SECRET)) : generateRandomSecret(); crypto.init('64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347'); -export const loginHandler = (req: Request, res: Response) => { +export const loginHandler =async (req: Request, res: Response) => { const password = req.body && req.body.password const hashedPass = crypto.hash(password); + const ip = String(req.headers['x-forwarded-for'] || req.socket.remoteAddress); // Exec the CLI validator login command - execFile('operator-cli', ['gui', 'login', hashedPass], (err, stdout, stderr) => { + execFile('operator-cli', ['gui', 'login', hashedPass,ip], (err, stdout, stderr) => { if (err) { cliStderrResponse(res, 'Unable to check login', err.message) return @@ -34,7 +35,10 @@ export const loginHandler = (req: Request, res: Response) => { } const cliResponse = yaml.load(stdout) - + if(cliResponse.login === 'blocked'){ + res.send({block:true}) + return; + } if (cliResponse.login !== 'authorized') { unautorizedResponse(req, res) return diff --git a/pages/login/index.tsx b/pages/login/index.tsx index 8730493..60d5ae6 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -21,6 +21,7 @@ const Login = () => { const {register, handleSubmit, formState} = useForm() const [apiError, setApiError] = useState(null); + const [isDisabled,setIsDisabled] = useState(false); async function onSubmit({password}: FieldValues) { setApiError(null); @@ -29,7 +30,10 @@ const Login = () => { await login(password) router.push('/') } - catch(error){ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + catch(error:any){ + if(error?.message === 'IpAddress has been blocked for too many failed Attempts') + setIsDisabled(true); setApiError(error as SetStateAction) } } @@ -52,7 +56,7 @@ const Login = () => {
{apiError.message}
)} - @@ -75,4 +79,4 @@ Login.getLayout = function getLayout(page: ReactElement) { } -export default Login +export default Login; diff --git a/services/auth.service.ts b/services/auth.service.ts index 80ee2b1..082a4a4 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -21,7 +21,12 @@ function useLogin() { throw new Error('Invalid password!'); } } - localStorage.setItem(isLoggedInKey, 'true'); + else if(res.ok && data?.block){ + throw new Error("IpAddress has been blocked for too many failed Attempts") + } + else{ localStorage.setItem(tokenKey, data.accessToken); + } + }, [apiBase]); } From aa691bbf97cfa552b30b36804df810fc572cc1b9 Mon Sep 17 00:00:00 2001 From: Tanuj Soni Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 02/13] Add cookie-based authentication Add cookie parser middleware Add cookie-parser middleware dependency Add isLogged flag to track user login status Fix gui logout functionality Refactor isLogged function --- hooks/useNodeLogs.ts | 2 +- services/auth.service.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hooks/useNodeLogs.ts b/hooks/useNodeLogs.ts index 3661a42..2c36928 100644 --- a/hooks/useNodeLogs.ts +++ b/hooks/useNodeLogs.ts @@ -25,7 +25,7 @@ export const useNodeLogs = (): NodeLogsResponse => { const downloadLog = (logName: string): void => { fetch(`${apiBase}/api/node/logs/${logName}`, { method: "GET", - credentials: 'include', + credentials: 'include' }) .then((response) => response.blob()) .then((blob) => { diff --git a/services/auth.service.ts b/services/auth.service.ts index 082a4a4..f6af4a7 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -24,7 +24,8 @@ function useLogin() { else if(res.ok && data?.block){ throw new Error("IpAddress has been blocked for too many failed Attempts") } - else{ localStorage.setItem(tokenKey, data.accessToken); + else{ + localStorage.setItem(isLoggedInKey, 'true'); } }, [apiBase]); From a5e338cfa1080fde3180f03098cc1886f33662c9 Mon Sep 17 00:00:00 2001 From: Tanuj Soni Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 03/13] Add expiration time for access token and cookie --- api/auth.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/auth.ts b/api/auth.ts index 36f33cf..ddecc02 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -45,10 +45,14 @@ export const loginHandler =async (req: Request, res: Response) => { } const accessToken = jwt.sign({ nodeId: '' /** add unique node id */ }, jwtSecret, { expiresIn: '8h' }) + const cookieExpiration = new Date(); + cookieExpiration.setTime(cookieExpiration.getTime() + (8 * 60 * 60 * 1000)); + res.cookie("accessToken", accessToken, { httpOnly: true, secure: true, sameSite: "strict", + expires: cookieExpiration, }); res.send({ status : 'ok' }) }) From adcc9d417b824de243cb1286f3d1ac112de714a1 Mon Sep 17 00:00:00 2001 From: Tanuj Soni Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 04/13] Remove cookie expiration in loginHandler --- api/auth.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index ddecc02..36f33cf 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -45,14 +45,10 @@ export const loginHandler =async (req: Request, res: Response) => { } const accessToken = jwt.sign({ nodeId: '' /** add unique node id */ }, jwtSecret, { expiresIn: '8h' }) - const cookieExpiration = new Date(); - cookieExpiration.setTime(cookieExpiration.getTime() + (8 * 60 * 60 * 1000)); - res.cookie("accessToken", accessToken, { httpOnly: true, secure: true, sameSite: "strict", - expires: cookieExpiration, }); res.send({ status : 'ok' }) }) From ab8bb0c4051d5ef55fcdf46ba9eaffc5a28b294a Mon Sep 17 00:00:00 2001 From: jpratham Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 05/13] Using req.socket.remoteAddress instead of req[x-forwarded-for] for client IP --- api/auth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/auth.ts b/api/auth.ts index 36f33cf..1a7f455 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -22,7 +22,8 @@ crypto.init('64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347'); export const loginHandler =async (req: Request, res: Response) => { const password = req.body && req.body.password const hashedPass = crypto.hash(password); - const ip = String(req.headers['x-forwarded-for'] || req.socket.remoteAddress); + const ip = String(req.socket.remoteAddress); + // Exec the CLI validator login command execFile('operator-cli', ['gui', 'login', hashedPass,ip], (err, stdout, stderr) => { if (err) { From cb473b1f92198e12b90a91b8012f4c0c54fd66e4 Mon Sep 17 00:00:00 2001 From: Thura Moe Myint Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 06/13] Implement a race condition check --- api/auth.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/auth.ts b/api/auth.ts index 1a7f455..ecb283e 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -19,13 +19,20 @@ const jwtSecret = (isValidSecret(process.env.JWT_SECRET)) : generateRandomSecret(); crypto.init('64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347'); +const MAX_CONCURRENT_EXEC = 1; +let currentExecCount = 0; export const loginHandler =async (req: Request, res: Response) => { + if (currentExecCount >= MAX_CONCURRENT_EXEC) { + res.status(503).send({ error: "Server is too busy. Please try again later." }); + return; + } const password = req.body && req.body.password const hashedPass = crypto.hash(password); const ip = String(req.socket.remoteAddress); // Exec the CLI validator login command execFile('operator-cli', ['gui', 'login', hashedPass,ip], (err, stdout, stderr) => { + currentExecCount--; if (err) { cliStderrResponse(res, 'Unable to check login', err.message) return From 22ce0f35e3946ccf8716d797d84ec3780e029c2c Mon Sep 17 00:00:00 2001 From: Mehdi Sabraoui Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 07/13] fixing my merge branch mistake --- api/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index ecb283e..6703526 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -23,7 +23,7 @@ const MAX_CONCURRENT_EXEC = 1; let currentExecCount = 0; export const loginHandler =async (req: Request, res: Response) => { if (currentExecCount >= MAX_CONCURRENT_EXEC) { - res.status(503).send({ error: "Server is too busy. Please try again later." }); + res.status(429).send({ error: "Server is too busy. Please try again later." }); return; } const password = req.body && req.body.password @@ -44,7 +44,7 @@ export const loginHandler =async (req: Request, res: Response) => { const cliResponse = yaml.load(stdout) if(cliResponse.login === 'blocked'){ - res.send({block:true}) + res.status(401).send({block:true}) return; } if (cliResponse.login !== 'authorized') { From 68218e8dd000757018ea4d4bdf260e6e3763b202 Mon Sep 17 00:00:00 2001 From: Mehdi Sabraoui Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 08/13] updating vulnerable dep --- package-lock.json | 96 ++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8893424..bfe7c81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -195,11 +195,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -1552,27 +1552,49 @@ } }, "node_modules/@solana/web3.js": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.78.0.tgz", - "integrity": "sha512-CSjCjo+RELJ5puoZALfznN5EF0YvL1V8NQrQYovsdjE1lCV6SqbKAIZD0+9LlqCBoa1ibuUaR7G2SooYzvzmug==", - "dependencies": { - "@babel/runtime": "^7.22.3", - "@noble/curves": "^1.0.0", - "@noble/hashes": "^1.3.0", - "@solana/buffer-layout": "^4.0.0", - "agentkeepalive": "^4.2.1", + "version": "1.91.7", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.91.7.tgz", + "integrity": "sha512-HqljZKDwk6Z4TajKRGhGLlRsbGK4S8EY27DA7v1z6yakewiUY3J7ZKDZRxcqz2MYV/ZXRrJ6wnnpiHFkPdv0WA==", + "dependencies": { + "@babel/runtime": "^7.23.4", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.3.3", + "@solana/buffer-layout": "^4.0.1", + "agentkeepalive": "^4.5.0", "bigint-buffer": "^1.1.5", - "bn.js": "^5.0.0", + "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.0", - "node-fetch": "^2.6.11", + "node-fetch": "^2.7.0", "rpc-websockets": "^7.5.1", "superstruct": "^0.14.2" } }, + "node_modules/@solana/web3.js/node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@solana/web3.js/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@stablelib/aead": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", @@ -3108,12 +3130,10 @@ "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" }, "node_modules/agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", "humanize-ms": "^1.2.1" }, "engines": { @@ -5084,9 +5104,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5172,9 +5192,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -5284,9 +5304,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -8087,9 +8107,9 @@ "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -9139,9 +9159,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -9202,9 +9222,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp-tree": { "version": "0.1.27", From ca6c0f9cf125ab9ecb7006e76f8642c996e78d47 Mon Sep 17 00:00:00 2001 From: Mehdi Sabraoui Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 09/13] added missing data var --- services/auth.service.ts | 70 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/services/auth.service.ts b/services/auth.service.ts index f6af4a7..648397c 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -1,52 +1,54 @@ -import Router from 'next/router' -import {useGlobals} from '../utils/globals' -import {hashSha256} from '../utils/sha256-hash'; -import {useCallback} from "react"; +import Router from "next/router"; +import { useGlobals } from "../utils/globals"; +import { hashSha256 } from "../utils/sha256-hash"; +import { useCallback } from "react"; -const isLoggedInKey = 'isLoggedIn' +const isLoggedInKey = "isLoggedIn"; function useLogin() { const { apiBase } = useGlobals(); - return useCallback(async (password: string) => { - const sha256digest = await hashSha256(password); - const res = await fetch(`${apiBase}/auth/login`, { - headers: {'Content-Type': 'application/json'}, - method: 'POST', - body: JSON.stringify({password: sha256digest}), - }); - await res.json(); - if (!res.ok) { - if (res.status === 403) { - throw new Error('Invalid password!'); + return useCallback( + async (password: string) => { + const sha256digest = await hashSha256(password); + const res = await fetch(`${apiBase}/auth/login`, { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify({ password: sha256digest }), + }); + const data = await res.json(); + if (!res.ok) { + if (res.status === 403) { + throw new Error("Invalid password!"); + } + } else if (res.ok && data?.block) { + throw new Error( + "IpAddress has been blocked for too many failed Attempts" + ); + } else { + localStorage.setItem(isLoggedInKey, "true"); } - } - else if(res.ok && data?.block){ - throw new Error("IpAddress has been blocked for too many failed Attempts") - } - else{ - localStorage.setItem(isLoggedInKey, 'true'); - } - - }, [apiBase]); + }, + [apiBase] + ); } -async function logout(apiBase : string) { +async function logout(apiBase: string) { const res = await fetch(`${apiBase}/auth/logout`, { - headers: {'Content-Type': 'application/json'}, - method: 'POST', - }) + headers: { "Content-Type": "application/json" }, + method: "POST", + }); if (res.status != 200) { - throw new Error('Error logging out!'); + throw new Error("Error logging out!"); } - localStorage.removeItem(isLoggedInKey) - Router.push('/login') + localStorage.removeItem(isLoggedInKey); + Router.push("/login"); } export const authService = { get isLogged(): boolean { - return !!localStorage.getItem(isLoggedInKey) + return !!localStorage.getItem(isLoggedInKey); }, useLogin, logout, -} +}; From b765a8e2a195a43a119320f681d74e32c9142958 Mon Sep 17 00:00:00 2001 From: jpratham Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 10/13] added changes to rate limit gui and show message below input box --- api/auth.ts | 147 ++++++++++++++++++++++++--------------- pages/login/index.tsx | 5 +- services/auth.service.ts | 18 ++--- 3 files changed, 102 insertions(+), 68 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index 6703526..c9f08c3 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -1,96 +1,127 @@ -import express, { Request, Response, NextFunction, response } from 'express' -import { execFile } from 'child_process' -import { cliStderrResponse, unautorizedResponse } from './handlers/util' -import * as crypto from '@shardus/crypto-utils'; -import rateLimit from 'express-rate-limit'; -const yaml = require('js-yaml') -const jwt = require('jsonwebtoken') +import express, { Request, Response, NextFunction, response } from "express"; +import { execFile } from "child_process"; +import { cliStderrResponse, unautorizedResponse } from "./handlers/util"; +import * as crypto from "@shardus/crypto-utils"; +import rateLimit from "express-rate-limit"; +const yaml = require("js-yaml"); +const jwt = require("jsonwebtoken"); function isValidSecret(secret: unknown) { - return typeof secret === 'string' && secret.length >= 32; + return typeof secret === "string" && secret.length >= 32; } function generateRandomSecret() { - return Buffer.from(crypto.randomBytes(32)).toString('hex'); + return Buffer.from(crypto.randomBytes(32)).toString("hex"); } -const jwtSecret = (isValidSecret(process.env.JWT_SECRET)) +const jwtSecret = isValidSecret(process.env.JWT_SECRET) ? process.env.JWT_SECRET : generateRandomSecret(); -crypto.init('64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347'); +crypto.init("64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347"); const MAX_CONCURRENT_EXEC = 1; let currentExecCount = 0; -export const loginHandler =async (req: Request, res: Response) => { +export const loginHandler = async (req: Request, res: Response) => { if (currentExecCount >= MAX_CONCURRENT_EXEC) { - res.status(429).send({ error: "Server is too busy. Please try again later." }); + res + .status(429) + .send({ error: "Server is too busy. Please try again later." }); return; } - const password = req.body && req.body.password + const password = req.body && req.body.password; const hashedPass = crypto.hash(password); const ip = String(req.socket.remoteAddress); - + // Exec the CLI validator login command - execFile('operator-cli', ['gui', 'login', hashedPass,ip], (err, stdout, stderr) => { - currentExecCount--; - if (err) { - cliStderrResponse(res, 'Unable to check login', err.message) - return - } - if (stderr) { - cliStderrResponse(res, 'Unable to check login', stderr) - return - } + execFile( + "operator-cli", + ["gui", "login", hashedPass, ip], + (err, stdout, stderr) => { + currentExecCount--; + if (err) { + cliStderrResponse(res, "Unable to check login", err.message); + return; + } + if (stderr) { + cliStderrResponse(res, "Unable to check login", stderr); + return; + } - const cliResponse = yaml.load(stdout) - if(cliResponse.login === 'blocked'){ - res.status(401).send({block:true}) - return; - } - if (cliResponse.login !== 'authorized') { - unautorizedResponse(req, res) - return - } - const accessToken = jwt.sign({ nodeId: '' /** add unique node id */ }, jwtSecret, { expiresIn: '8h' }) + const cliResponse = yaml.load(stdout); + if (cliResponse.login === "blocked") { + res.status(403).json({ errorMessage: "Blocked" }); + // Set a timeout to unlock the IP after 30 minutes + setTimeout( () => { + execFile( + "operator-cli", + ["gui", "unlock", ip], + (err, stdout, stderr) => { + if (err) { + console.error("Unable to unlock IP", err.message); + } else if (stderr) { + console.error("Unable to unlock IP", stderr); + } else { + console.log("IP unlocked successfully"); + } + } + ); + }, 30 * 60 * 1000); // 30 minutes in milliseconds - res.cookie("accessToken", accessToken, { - httpOnly: true, - secure: true, - sameSite: "strict", - }); - res.send({ status : 'ok' }) - }) - console.log('executing operator-cli gui login...') -} + return; + } + if (cliResponse.login !== "authorized") { + unautorizedResponse(req, res); + return; + } + const accessToken = jwt.sign( + { nodeId: "" /** add unique node id */ }, + jwtSecret, + { expiresIn: "8h" } + ); + + res.cookie("accessToken", accessToken, { + httpOnly: true, + secure: true, + sameSite: "strict", + }); + res.send({ status: "ok" }); + } + ); + console.log("executing operator-cli gui login..."); +}; export const logoutHandler = (req: Request, res: Response) => { res.clearCookie("accessToken"); - res.send({ status: 'ok' }) -} + res.send({ status: "ok" }); +}; export const apiLimiter = rateLimit({ windowMs: 10 * 60 * 1000, // 10 minutes max: 1500, // Limit each IP to 1500 requests per windowMs - message: 'Too many requests from this IP, please try again after 10 minutes', + message: "Too many requests from this IP, please try again after 10 minutes", }); -export const httpBodyLimiter = express.json({ limit: '100kb' }) - +export const httpBodyLimiter = express.json({ limit: "100kb" }); -export const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => { +export const jwtMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { const token = req.cookies.accessToken; if (!token) { - unautorizedResponse(req, res) - return + unautorizedResponse(req, res); + return; } jwt.verify(token, jwtSecret, (err: any, jwtData: any) => { - if (err) {// invalid token - unautorizedResponse(req, res) - return + if (err) { + // invalid token + unautorizedResponse(req, res); + return; } - next() - }) -} + next(); + }); +}; diff --git a/pages/login/index.tsx b/pages/login/index.tsx index 60d5ae6..7793f3e 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -32,8 +32,9 @@ const Login = () => { } // eslint-disable-next-line @typescript-eslint/no-explicit-any catch(error:any){ - if(error?.message === 'IpAddress has been blocked for too many failed Attempts') - setIsDisabled(true); + if(error.message === "IpAddress has been blocked for too many failed Attempts!"){ + setIsDisabled(true); + } setApiError(error as SetStateAction) } } diff --git a/services/auth.service.ts b/services/auth.service.ts index 648397c..b18ced3 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -18,14 +18,16 @@ function useLogin() { }); const data = await res.json(); if (!res.ok) { - if (res.status === 403) { - throw new Error("Invalid password!"); - } - } else if (res.ok && data?.block) { - throw new Error( - "IpAddress has been blocked for too many failed Attempts" - ); - } else { + console.log(res, data); + if (res.status === 403 && data?.errorMessage === "Blocked") { + throw new Error( + "IpAddress has been blocked for too many failed Attempts!" + ); + } else if (res.status === 403) { + throw new Error("Invalid password!"); + } + } + else { localStorage.setItem(isLoggedInKey, "true"); } }, From 476ee211fab5dd2fc938c51e4221d41618db54b9 Mon Sep 17 00:00:00 2001 From: jpratham Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 11/13] added changes to check ip is whether blocked or not --- api/auth.ts | 27 ++++++++ api/index.ts | 3 +- pages/login/index.tsx | 138 ++++++++++++++++++++++++++------------- services/auth.service.ts | 18 +++-- 4 files changed, 134 insertions(+), 52 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index c9f08c3..397c120 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -100,7 +100,34 @@ export const apiLimiter = rateLimit({ max: 1500, // Limit each IP to 1500 requests per windowMs message: "Too many requests from this IP, please try again after 10 minutes", }); +export const checkIpHandler = (req:Request,res:Response) =>{ + const ip = String(req.socket.remoteAddress); +// Exec the CLI validator check-ip command +execFile( + "operator-cli", + ["gui", "ipStatus", ip], + (err, stdout, stderr) => { + currentExecCount--; + if (err) { + cliStderrResponse(res, "Unable to check ip", err.message); + return; + } + if (stderr) { + cliStderrResponse(res, "Unable to check ip", stderr); + return; + } + const cliResponse = yaml.load(stdout); + if(cliResponse.status === "blocked"){ + res.status(200).json({ip: "blocked"}) + } + else{ + res.status(200).json({ip: "unblocked"}) + } + } +); +console.log("executing operator-cli gui check-ip..."); +} export const httpBodyLimiter = express.json({ limit: "100kb" }); export const jwtMiddleware = ( diff --git a/api/index.ts b/api/index.ts index 5dd2afc..66ea78a 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,5 +1,5 @@ import apiRouter from './api' -import { apiLimiter, httpBodyLimiter, jwtMiddleware, loginHandler, logoutHandler } from './auth' +import { apiLimiter, checkIpHandler, httpBodyLimiter, jwtMiddleware, loginHandler, logoutHandler } from './auth' import * as https from 'https'; import * as fs from 'fs'; import path from 'path'; @@ -24,6 +24,7 @@ if (isDev) { app.use(cookieParser()); app.post('/auth/login', loginHandler) app.post('/auth/logout', logoutHandler) + app.post('/auth/check',checkIpHandler) app.use('/api', jwtMiddleware, apiRouter) app.get('*', (req: any, res: any) => nextHandler(req, res)) app.use(errorMiddleware(isDev)) diff --git a/pages/login/index.tsx b/pages/login/index.tsx index 7793f3e..a410143 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -1,83 +1,129 @@ -import { ReactElement, SetStateAction, useState } from 'react' -import { useEffect } from 'react' -import { useRouter } from 'next/router' -import { FieldValues, useForm } from 'react-hook-form' -import { ArrowPathIcon } from '@heroicons/react/20/solid' -import { authService } from '../../services/auth.service'; -import Head from 'next/head'; +import { ReactElement, SetStateAction, useState } from "react"; +import { useEffect } from "react"; +import { useRouter } from "next/router"; +import { FieldValues, useForm } from "react-hook-form"; +import { ArrowPathIcon } from "@heroicons/react/20/solid"; +import { authService } from "../../services/auth.service"; +import { useGlobals } from "../../utils/globals"; + +import Head from "next/head"; const Login = () => { - const router = useRouter() - const login = authService.useLogin() + const router = useRouter(); + const { apiBase } = useGlobals(); + const { register, handleSubmit, formState } = useForm(); + + const [apiError, setApiError] = useState(null); + const [isDisabled, setIsDisabled] = useState(false); + const login = authService.useLogin(); useEffect(() => { // redirect to home if already logged in if (authService.isLogged) { - router.push('/') + router.push("/"); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + // check if ip is blocked + const intervalId = setInterval(async () => { + try { + const status = await authService.checkIp(apiBase); + if(status.ip === "blocked"){ + setIsDisabled(true) + setApiError(Error("IpAddress has been blocked for too many failed Attempts!")) + } + else{ + setIsDisabled(false) + setApiError(null) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + setApiError(err as SetStateAction); + } + }, 10 * 1000); // 1 minute in milliseconds - const {register, handleSubmit, formState} = useForm() + // Clear interval on component unmount + return () => clearInterval(intervalId); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const [apiError, setApiError] = useState(null); - const [isDisabled,setIsDisabled] = useState(false); - async function onSubmit({password}: FieldValues) { + async function onSubmit({ password }: FieldValues) { setApiError(null); - try{ - await login(password) - router.push('/') - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - catch(error:any){ - if(error.message === "IpAddress has been blocked for too many failed Attempts!"){ + try { + await login(password); + router.push("/"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if ( + error.message === + "IpAddress has been blocked for too many failed Attempts!" + ) { setIsDisabled(true); } - setApiError(error as SetStateAction) + setApiError(error as SetStateAction); } } return ( <> {/* eslint-disable-next-line @next/next/no-img-element */} - Logo + Logo
-

Connect to Validator Dashboard

+

+ Connect to Validator Dashboard +

- Connect to your validator dashboard to see the performance of your node, check rewards and run - maintenance tasks! + Connect to your validator dashboard to see the performance of your + node, check rewards and run maintenance tasks!

- + {apiError && (
{apiError.message}
)} -
- ) -} + ); +}; Login.getLayout = function getLayout(page: ReactElement) { - return <> - - Shardeum Dashboard - - - - -
- {page} -
- -} + return ( + <> + + Shardeum Dashboard + + + + +
{page}
+ + ); +}; export default Login; diff --git a/services/auth.service.ts b/services/auth.service.ts index b18ced3..e0304dc 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -18,16 +18,14 @@ function useLogin() { }); const data = await res.json(); if (!res.ok) { - console.log(res, data); if (res.status === 403 && data?.errorMessage === "Blocked") { throw new Error( "IpAddress has been blocked for too many failed Attempts!" ); } else if (res.status === 403) { - throw new Error("Invalid password!"); - } - } - else { + throw new Error("Invalid password!"); + } + } else { localStorage.setItem(isLoggedInKey, "true"); } }, @@ -47,10 +45,20 @@ async function logout(apiBase: string) { Router.push("/login"); } +async function checkIp(apiBase: string) { + const res = await fetch(`${apiBase}/auth/check`, { + headers: { "Content-Type": "application/json" }, + method: "POST", + }); + const data = await res.json(); + return data +} + export const authService = { get isLogged(): boolean { return !!localStorage.getItem(isLoggedInKey); }, useLogin, logout, + checkIp }; From daaffa152e02d7bdd493a0dbecd9015c86df50f7 Mon Sep 17 00:00:00 2001 From: jpratham Date: Mon, 15 Jul 2024 13:44:01 +0530 Subject: [PATCH 12/13] change the time interval to be of 1 minute from 10 seconds --- pages/login/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/login/index.tsx b/pages/login/index.tsx index a410143..2ea426b 100644 --- a/pages/login/index.tsx +++ b/pages/login/index.tsx @@ -38,7 +38,7 @@ const Login = () => { } catch (err: any) { setApiError(err as SetStateAction); } - }, 10 * 1000); // 1 minute in milliseconds + }, 60 * 1000); // 1 minute in milliseconds // Clear interval on component unmount return () => clearInterval(intervalId); From 6237d37885daec3226021db517e65ca5998c710e Mon Sep 17 00:00:00 2001 From: jpratham Date: Tue, 16 Jul 2024 17:34:52 +0530 Subject: [PATCH 13/13] Added unlocking logs in gui --- api/auth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/auth.ts b/api/auth.ts index 397c120..24436ff 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -56,6 +56,7 @@ export const loginHandler = async (req: Request, res: Response) => { "operator-cli", ["gui", "unlock", ip], (err, stdout, stderr) => { + console.log("executing operator-cli gui unlock"); if (err) { console.error("Unable to unlock IP", err.message); } else if (stderr) { @@ -126,7 +127,7 @@ execFile( } } ); -console.log("executing operator-cli gui check-ip..."); +console.log("executing operator-cli gui ipStatus"); } export const httpBodyLimiter = express.json({ limit: "100kb" });