Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUI Login Rate Limiting #19

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 122 additions & 51 deletions api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,155 @@
import express, { Request, Response, NextFunction } 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");

export const loginHandler = (req: Request, res: Response) => {
const password = req.body && req.body.password
const MAX_CONCURRENT_EXEC = 1;
let currentExecCount = 0;
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." });
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], (err, stdout, stderr) => {
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)
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) => {
console.log("executing operator-cli gui unlock");
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

if (cliResponse.login !== 'authorized') {
unautorizedResponse(req, res)
return
}
const accessToken = jwt.sign({ nodeId: '' /** add unique node id */ }, jwtSecret, { expiresIn: '8h' })
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...')
}
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 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;
}

export const httpBodyLimiter = express.json({ limit: '100kb' })

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 ipStatus");
}
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();
});
};
3 changes: 2 additions & 1 deletion api/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion hooks/useNodeLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading