From 31fc8f37dbe6e75eddcd0e404ef1d3570b416735 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Mon, 28 Aug 2023 18:41:17 +0800 Subject: [PATCH 1/9] rate limit poc --- packages/server/package.json | 1 + packages/server/src/index.ts | 18 ++++++++- packages/server/src/utils/rateLimit.ts | 56 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/utils/rateLimit.ts diff --git a/packages/server/package.json b/packages/server/package.json index 4d50ddc4..9b9b7dbd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -52,6 +52,7 @@ "dotenv": "^16.0.0", "express": "^4.17.3", "express-basic-auth": "^1.2.1", + "express-rate-limit": "^6.9.0", "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 66c7b000..15709ad9 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from 'express' +import express, { NextFunction, Request, Response } from 'express' import multer from 'multer' import path from 'path' import cors from 'cors' @@ -54,6 +54,7 @@ import { Credential } from './entity/Credential' import { Tool } from './entity/Tool' import { ChatflowPool } from './ChatflowPool' import { ICommonObject, INodeOptionsValue } from 'flowise-components' +import { createRateLimiter, getRateLimiter } from './utils/rateLimit' export class App { app: express.Application @@ -654,6 +655,21 @@ export class App { // Prediction // ---------------------------------------- + this.app.get( + '/api/v1/rate-limit/:id', + upload.array('files'), + (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), + // specificRouteLimiter, + async (req: Request, res: Response) => { + res.send("you're fine") + } + ) + + this.app.post('/api/v1/rate-limit/', async (req: Request, res: Response) => { + createRateLimiter(req) + res.send('Created/Updated rate limit') + }) + // Send input message and get prediction result (External) this.app.post('/api/v1/prediction/:id', upload.array('files'), async (req: Request, res: Response) => { await this.processPrediction(req, res, socketIO) diff --git a/packages/server/src/utils/rateLimit.ts b/packages/server/src/utils/rateLimit.ts new file mode 100644 index 00000000..0bd5be98 --- /dev/null +++ b/packages/server/src/utils/rateLimit.ts @@ -0,0 +1,56 @@ +import { NextFunction, Request, Response } from 'express' +import { rateLimit, RateLimitRequestHandler } from 'express-rate-limit' + +interface RateLimit { + id: string + rateLimitObj: RateLimitRequestHandler +} + +export const specificRouteLimiter: RateLimitRequestHandler = rateLimit({ + windowMs: 1 * 60 * 1000, // 15 minutes + max: 1, // Limit each IP to 100 requests per windowMs + message: 'Too many requests, please try again later.' +}) + +let rateLimiters: RateLimit[] = [] + +export function createRateLimiter(req: Request) { + const id = req.body.id + const duration = req.body.duration + const limit = req.body.limit + const message = req.body.message + + const rateLimitObj: RateLimitRequestHandler = rateLimit({ + windowMs: Number(duration), + max: limit, + handler: (req, res) => { + res.status(429).json({ error: message }) + } + }) + + const existingIndex: number = rateLimiters.findIndex((rateLimit) => rateLimit.id === id) + + if (existingIndex === -1) { + rateLimiters.push({ + id, + rateLimitObj + }) + } else { + rateLimiters[existingIndex] = { + id, + rateLimitObj + } + } +} + +export function getRateLimiter(req: Request, res: Response, next: NextFunction) { + const id = req.params.id + + const ratelimiter = rateLimiters.find((rateLimit) => rateLimit.id === id) + + if (!ratelimiter) return next() + + const idRateLimiter = ratelimiter.rateLimitObj + + return idRateLimiter(req, res, next) +} From 1b75121d5e1939aba7b22fddcb9f4550ad93ec41 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Wed, 30 Aug 2023 21:35:16 +0800 Subject: [PATCH 2/9] init rateLimiter --- packages/server/package.json | 1 + packages/server/src/Interface.ts | 3 ++ packages/server/src/entity/ChatFlow.ts | 9 ++++ packages/server/src/index.ts | 60 +++++++++++++++++------- packages/server/src/utils/rateLimit.ts | 65 ++++++++++---------------- 5 files changed, 80 insertions(+), 58 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 9b9b7dbd..4d4293b0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -46,6 +46,7 @@ "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@oclif/core": "^1.13.10", + "async-mutex": "^0.4.0", "axios": "^0.27.2", "cors": "^2.8.5", "crypto-js": "^4.1.1", diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 92e3054d..a83e556e 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -15,6 +15,9 @@ export interface IChatFlow { isPublic?: boolean apikeyid?: string chatbotConfig?: string + rateLimit?: number + rateLimitDuration?: number + rateLimitMsg?: string } export interface IChatMessage { diff --git a/packages/server/src/entity/ChatFlow.ts b/packages/server/src/entity/ChatFlow.ts index 4c37e083..e8ed861b 100644 --- a/packages/server/src/entity/ChatFlow.ts +++ b/packages/server/src/entity/ChatFlow.ts @@ -25,6 +25,15 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true }) chatbotConfig?: string + @Column({ nullable: true }) + rateLimit?: number + + @Column({ nullable: true }) + rateLimitDuration?: number + + @Column({ nullable: true }) + rateLimitMsg?: string + @CreateDateColumn() createdDate: Date diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 15709ad9..f6df0c30 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -54,7 +54,7 @@ import { Credential } from './entity/Credential' import { Tool } from './entity/Tool' import { ChatflowPool } from './ChatflowPool' import { ICommonObject, INodeOptionsValue } from 'flowise-components' -import { createRateLimiter, getRateLimiter } from './utils/rateLimit' +import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' export class App { app: express.Application @@ -84,6 +84,10 @@ export class App { // Initialize encryption key await getEncryptionKey() + + // Initialize Rate Limit + const AllChatFlow: IChatFlow[] = await getAllChatFlow() + await initializeRateLimiter(AllChatFlow) }) .catch((err) => { logger.error('❌ [server]: Error during Data Source initialization:', err) @@ -246,7 +250,7 @@ export class App { // Get all chatflows this.app.get('/api/v1/chatflows', async (req: Request, res: Response) => { - const chatflows: IChatFlow[] = await this.AppDataSource.getRepository(ChatFlow).find() + const chatflows: IChatFlow[] = await getAllChatFlow() return res.json(chatflows) }) @@ -655,21 +659,6 @@ export class App { // Prediction // ---------------------------------------- - this.app.get( - '/api/v1/rate-limit/:id', - upload.array('files'), - (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), - // specificRouteLimiter, - async (req: Request, res: Response) => { - res.send("you're fine") - } - ) - - this.app.post('/api/v1/rate-limit/', async (req: Request, res: Response) => { - createRateLimiter(req) - res.send('Created/Updated rate limit') - }) - // Send input message and get prediction result (External) this.app.post('/api/v1/prediction/:id', upload.array('files'), async (req: Request, res: Response) => { await this.processPrediction(req, res, socketIO) @@ -768,6 +757,39 @@ export class App { } }) + // ---------------------------------------- + // Rate Limit + // ---------------------------------------- + + this.app.get( + '/api/v1/rate-limit/:id', + upload.array('files'), + (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), + // specificRouteLimiter, + async (req: Request, res: Response) => { + res.send("you're fine") + } + ) + + this.app.post('/api/v1/rate-limit/', async (req: Request, res: Response) => { + const id = req.body.id + const duration = req.body.duration + const limit = req.body.limit + const message = req.body.message + + const result = await getDataSource() + .getRepository(ChatFlow) + .createQueryBuilder() + .update(ChatFlow) + .set({ rateLimit: limit, rateLimitDuration: duration, rateLimitMsg: message }) + .where('id = :id', { id: id }) + .execute() + + await createRateLimiter(id, Number(duration), Number(limit), message) + + res.send({ result }) + }) + // ---------------------------------------- // Serve UI static // ---------------------------------------- @@ -1012,6 +1034,10 @@ export async function getChatId(chatflowid: string) { let serverApp: App | undefined +export async function getAllChatFlow(): Promise { + return await getDataSource().getRepository(ChatFlow).find() +} + export async function start(): Promise { serverApp = new App() diff --git a/packages/server/src/utils/rateLimit.ts b/packages/server/src/utils/rateLimit.ts index 0bd5be98..882964c4 100644 --- a/packages/server/src/utils/rateLimit.ts +++ b/packages/server/src/utils/rateLimit.ts @@ -1,56 +1,39 @@ import { NextFunction, Request, Response } from 'express' import { rateLimit, RateLimitRequestHandler } from 'express-rate-limit' +import { IChatFlow } from '../Interface' +import { Mutex } from 'async-mutex' -interface RateLimit { - id: string - rateLimitObj: RateLimitRequestHandler -} +let rateLimiters: Record = {} +const rateLimiterMutex = new Mutex() -export const specificRouteLimiter: RateLimitRequestHandler = rateLimit({ - windowMs: 1 * 60 * 1000, // 15 minutes - max: 1, // Limit each IP to 100 requests per windowMs - message: 'Too many requests, please try again later.' -}) - -let rateLimiters: RateLimit[] = [] - -export function createRateLimiter(req: Request) { - const id = req.body.id - const duration = req.body.duration - const limit = req.body.limit - const message = req.body.message - - const rateLimitObj: RateLimitRequestHandler = rateLimit({ - windowMs: Number(duration), - max: limit, - handler: (req, res) => { - res.status(429).json({ error: message }) - } - }) - - const existingIndex: number = rateLimiters.findIndex((rateLimit) => rateLimit.id === id) - - if (existingIndex === -1) { - rateLimiters.push({ - id, - rateLimitObj +export async function createRateLimiter(id: string, duration: number, limit: number, message: string) { + const release = await rateLimiterMutex.acquire() + try { + rateLimiters[id] = rateLimit({ + windowMs: duration, + max: limit, + handler: (req, res) => { + res.status(429).json({ error: message }) + } }) - } else { - rateLimiters[existingIndex] = { - id, - rateLimitObj - } + } finally { + release() } } export function getRateLimiter(req: Request, res: Response, next: NextFunction) { const id = req.params.id - const ratelimiter = rateLimiters.find((rateLimit) => rateLimit.id === id) + if (!rateLimiters[id]) return next() - if (!ratelimiter) return next() - - const idRateLimiter = ratelimiter.rateLimitObj + const idRateLimiter = rateLimiters[id] return idRateLimiter(req, res, next) } + +export async function initializeRateLimiter(ChatFlowPool: IChatFlow[]) { + await ChatFlowPool.map(async (ChatFlow) => { + if (ChatFlow.rateLimitDuration && ChatFlow.rateLimit && ChatFlow.rateLimitMsg) + await createRateLimiter(ChatFlow.id, ChatFlow.rateLimitDuration, ChatFlow.rateLimit, ChatFlow.rateLimitMsg) + }) +} From a58adcadf1087cb6d858b90647611fba57fcfa76 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Wed, 30 Aug 2023 21:43:42 +0800 Subject: [PATCH 3/9] add rateLimiter to prediction --- packages/server/src/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f6df0c30..b72cbaa1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -660,9 +660,14 @@ export class App { // ---------------------------------------- // Send input message and get prediction result (External) - this.app.post('/api/v1/prediction/:id', upload.array('files'), async (req: Request, res: Response) => { - await this.processPrediction(req, res, socketIO) - }) + this.app.post( + '/api/v1/prediction/:id', + upload.array('files'), + (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), + async (req: Request, res: Response) => { + await this.processPrediction(req, res, socketIO) + } + ) // Send input message and get prediction result (Internal) this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { @@ -765,7 +770,6 @@ export class App { '/api/v1/rate-limit/:id', upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), - // specificRouteLimiter, async (req: Request, res: Response) => { res.send("you're fine") } From cbf17b3624566a87dbb5c8196fa5a590c375e7eb Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Thu, 31 Aug 2023 00:45:15 +0800 Subject: [PATCH 4/9] standardize rate limit return error message --- packages/server/src/utils/rateLimit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/rateLimit.ts b/packages/server/src/utils/rateLimit.ts index 882964c4..25222afd 100644 --- a/packages/server/src/utils/rateLimit.ts +++ b/packages/server/src/utils/rateLimit.ts @@ -13,7 +13,7 @@ export async function createRateLimiter(id: string, duration: number, limit: num windowMs: duration, max: limit, handler: (req, res) => { - res.status(429).json({ error: message }) + res.status(429).send(message) } }) } finally { From 607415c663868b82d4cb87fa5ae26ad110f81879 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 2 Sep 2023 13:28:47 +0800 Subject: [PATCH 5/9] add API Configuration UI --- packages/ui/src/assets/images/settings.svg | 5 + .../ui/src/views/chatflows/APICodeDialog.js | 9 +- .../ui/src/views/chatflows/Configuration.js | 145 ++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/assets/images/settings.svg create mode 100644 packages/ui/src/views/chatflows/Configuration.js diff --git a/packages/ui/src/assets/images/settings.svg b/packages/ui/src/assets/images/settings.svg new file mode 100644 index 00000000..4f4dfc09 --- /dev/null +++ b/packages/ui/src/assets/images/settings.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/ui/src/views/chatflows/APICodeDialog.js b/packages/ui/src/views/chatflows/APICodeDialog.js index 0ae8a8f4..49c718cc 100644 --- a/packages/ui/src/views/chatflows/APICodeDialog.js +++ b/packages/ui/src/views/chatflows/APICodeDialog.js @@ -34,6 +34,7 @@ import javascriptSVG from 'assets/images/javascript.svg' import cURLSVG from 'assets/images/cURL.svg' import EmbedSVG from 'assets/images/embed.svg' import ShareChatbotSVG from 'assets/images/sharing.png' +import settingsSVG from 'assets/images/settings.svg' // API import apiKeyApi from 'api/apikey' @@ -46,6 +47,7 @@ import { CheckboxInput } from 'ui-component/checkbox/Checkbox' import { TableViewOnly } from 'ui-component/table/Table' import { IconBulb } from '@tabler/icons' +import Configuration from './Configuration' function TabPanel(props) { const { children, value, index, ...other } = props @@ -141,7 +143,7 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => { const navigate = useNavigate() const dispatch = useDispatch() - const codes = ['Embed', 'Python', 'JavaScript', 'cURL', 'Share Chatbot'] + const codes = ['Embed', 'Python', 'JavaScript', 'cURL', 'Share Chatbot', 'Configuration'] const [value, setValue] = useState(0) const [keyOptions, setKeyOptions] = useState([]) const [apiKeys, setAPIKeys] = useState([]) @@ -321,6 +323,8 @@ query({"question": "Hey, how are you?"}).then((response) => { return cURLSVG } else if (codeLang === 'Share Chatbot') { return ShareChatbotSVG + } else if (codeLang === 'Configuration') { + return settingsSVG } return pythonSVG } @@ -647,7 +651,7 @@ formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")` )} {codeLang === 'Embed' && !chatflowApiKeyId && } - {codeLang !== 'Embed' && codeLang !== 'Share Chatbot' && ( + {codeLang !== 'Embed' && codeLang !== 'Share Chatbot' && codeLang !== 'Configuration' && ( <> )} + {codeLang === 'Configuration' && } ))} diff --git a/packages/ui/src/views/chatflows/Configuration.js b/packages/ui/src/views/chatflows/Configuration.js new file mode 100644 index 00000000..d5f4bbf9 --- /dev/null +++ b/packages/ui/src/views/chatflows/Configuration.js @@ -0,0 +1,145 @@ +import { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions' +import PropTypes from 'prop-types' + +import { Box, Typography, Button, OutlinedInput } from '@mui/material' + +// Project import +import { StyledButton } from 'ui-component/button/StyledButton' + +// Icons +import { IconX } from '@tabler/icons' + +// API +import chatflowsApi from 'api/chatflows' + +// utils +import useNotifier from 'utils/useNotifier' + +const Configuration = () => { + const dispatch = useDispatch() + const chatflow = useSelector((state) => state.canvas.chatflow) + const chatflowid = chatflow.id + const apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {} + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [limitMax, setLimitMax] = useState(apiConfig?.rateLimit?.limitMax ?? 20) + const [limitDuration, setLimitDuration] = useState(apiConfig?.rateLimit?.limitDuration ?? 120) + const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? "Please don't spam me") + + const formatObj = () => { + const obj = { + rateLimit: {} + } + + if (limitMax && limitDuration && limitMsg) + obj.rateLimit = { + limitMax, + limitDuration, + limitMsg + } + + return obj + } + + const onSave = async () => { + try { + const saveResp = await chatflowsApi.updateChatflow(chatflowid, { + apiConfig: JSON.stringify(formatObj()) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'API Configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + console.error(error) + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save API Configuration: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const onTextChanged = (value, fieldName) => { + switch (fieldName) { + case 'limitMax': + setLimitMax(value) + break + case 'limitDuration': + setLimitDuration(value) + break + case 'limitMsg': + setLimitMsg(value) + break + } + } + + const textField = (message, fieldName, fieldLabel, fieldType = 'string', placeholder = '') => { + return ( + +
+ {fieldLabel} + { + onTextChanged(e.target.value, fieldName) + }} + /> +
+
+ ) + } + + return ( + <> + {/*Rate Limit*/} + + Rate Limit + + {textField(limitMax, 'Limit Max', 'Max Limit', 'number')} + {textField(limitDuration, 'Limit Duration', 'Font Size', 'number')} + {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} + + onSave()}> + Save Changes + + + ) +} + +Configuration.propTypes = { + isSessionMemory: PropTypes.bool +} + +export default Configuration From 34dd7a3a3d00915b7411e644d54059fe1becc32b Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 2 Sep 2023 14:59:33 +0800 Subject: [PATCH 6/9] modify rateLimit data storage --- packages/server/src/Interface.ts | 4 +-- packages/server/src/entity/ChatFlow.ts | 8 +---- packages/server/src/index.ts | 35 ++----------------- packages/server/src/utils/rateLimit.ts | 20 +++++++---- .../ui/src/views/chatflows/Configuration.js | 4 +-- 5 files changed, 21 insertions(+), 50 deletions(-) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index a83e556e..42c1231a 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -15,9 +15,7 @@ export interface IChatFlow { isPublic?: boolean apikeyid?: string chatbotConfig?: string - rateLimit?: number - rateLimitDuration?: number - rateLimitMsg?: string + apiConfig?: any } export interface IChatMessage { diff --git a/packages/server/src/entity/ChatFlow.ts b/packages/server/src/entity/ChatFlow.ts index e8ed861b..2be09155 100644 --- a/packages/server/src/entity/ChatFlow.ts +++ b/packages/server/src/entity/ChatFlow.ts @@ -26,13 +26,7 @@ export class ChatFlow implements IChatFlow { chatbotConfig?: string @Column({ nullable: true }) - rateLimit?: number - - @Column({ nullable: true }) - rateLimitDuration?: number - - @Column({ nullable: true }) - rateLimitMsg?: string + apiConfig?: string @CreateDateColumn() createdDate: Date diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b72cbaa1..82e32bdb 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -319,6 +319,9 @@ export class App { const updateChatFlow = new ChatFlow() Object.assign(updateChatFlow, body) + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) @@ -762,38 +765,6 @@ export class App { } }) - // ---------------------------------------- - // Rate Limit - // ---------------------------------------- - - this.app.get( - '/api/v1/rate-limit/:id', - upload.array('files'), - (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), - async (req: Request, res: Response) => { - res.send("you're fine") - } - ) - - this.app.post('/api/v1/rate-limit/', async (req: Request, res: Response) => { - const id = req.body.id - const duration = req.body.duration - const limit = req.body.limit - const message = req.body.message - - const result = await getDataSource() - .getRepository(ChatFlow) - .createQueryBuilder() - .update(ChatFlow) - .set({ rateLimit: limit, rateLimitDuration: duration, rateLimitMsg: message }) - .where('id = :id', { id: id }) - .execute() - - await createRateLimiter(id, Number(duration), Number(limit), message) - - res.send({ result }) - }) - // ---------------------------------------- // Serve UI static // ---------------------------------------- diff --git a/packages/server/src/utils/rateLimit.ts b/packages/server/src/utils/rateLimit.ts index 25222afd..b1cd1819 100644 --- a/packages/server/src/utils/rateLimit.ts +++ b/packages/server/src/utils/rateLimit.ts @@ -6,11 +6,11 @@ import { Mutex } from 'async-mutex' let rateLimiters: Record = {} const rateLimiterMutex = new Mutex() -export async function createRateLimiter(id: string, duration: number, limit: number, message: string) { +async function addRateLimiter(id: string, duration: number, limit: number, message: string) { const release = await rateLimiterMutex.acquire() try { rateLimiters[id] = rateLimit({ - windowMs: duration, + windowMs: duration * 1000, max: limit, handler: (req, res) => { res.status(429).send(message) @@ -31,9 +31,17 @@ export function getRateLimiter(req: Request, res: Response, next: NextFunction) return idRateLimiter(req, res, next) } -export async function initializeRateLimiter(ChatFlowPool: IChatFlow[]) { - await ChatFlowPool.map(async (ChatFlow) => { - if (ChatFlow.rateLimitDuration && ChatFlow.rateLimit && ChatFlow.rateLimitMsg) - await createRateLimiter(ChatFlow.id, ChatFlow.rateLimitDuration, ChatFlow.rateLimit, ChatFlow.rateLimitMsg) +export async function createRateLimiter(chatFlow: IChatFlow) { + if (!chatFlow.apiConfig) return + const apiConfig: any = JSON.parse(chatFlow.apiConfig) + const rateLimit: { limitDuration: number; limitMax: number; limitMsg: string } = apiConfig.rateLimit + if (!rateLimit) return + const { limitDuration, limitMax, limitMsg } = rateLimit + if (limitMax && limitDuration && limitMsg) await addRateLimiter(chatFlow.id, limitDuration, limitMax, limitMsg) +} + +export async function initializeRateLimiter(chatFlowPool: IChatFlow[]) { + await chatFlowPool.map(async (chatFlow) => { + await createRateLimiter(chatFlow) }) } diff --git a/packages/ui/src/views/chatflows/Configuration.js b/packages/ui/src/views/chatflows/Configuration.js index d5f4bbf9..b49b87e7 100644 --- a/packages/ui/src/views/chatflows/Configuration.js +++ b/packages/ui/src/views/chatflows/Configuration.js @@ -127,8 +127,8 @@ const Configuration = () => { Rate Limit - {textField(limitMax, 'Limit Max', 'Max Limit', 'number')} - {textField(limitDuration, 'Limit Duration', 'Font Size', 'number')} + {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number')} + {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number')} {textField(limitMsg, 'limitMsg', 'Limit Message', 'string')} onSave()}> From 49f8e796f43e72c4901c42abff46dca9bb97fa08 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 2 Sep 2023 15:48:04 +0800 Subject: [PATCH 7/9] fix UI no data show default value --- packages/ui/src/views/chatflows/Configuration.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/views/chatflows/Configuration.js b/packages/ui/src/views/chatflows/Configuration.js index b49b87e7..4268fd08 100644 --- a/packages/ui/src/views/chatflows/Configuration.js +++ b/packages/ui/src/views/chatflows/Configuration.js @@ -36,13 +36,17 @@ const Configuration = () => { const obj = { rateLimit: {} } - - if (limitMax && limitDuration && limitMsg) + const rateLimitValuesBoolean = [!limitMax, !limitDuration, !limitMsg] + const rateLimitFilledValues = rateLimitValuesBoolean.filter((value) => value === false) + if (rateLimitFilledValues.length >= 1 && rateLimitFilledValues.length <= 2) { + throw new Error('Need to fill all rate limit input fields') + } else if (rateLimitFilledValues.length === 3) { obj.rateLimit = { limitMax, limitDuration, limitMsg } + } return obj } @@ -69,7 +73,9 @@ const Configuration = () => { } } catch (error) { console.error(error) - const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + const errorData = error.response + ? error.response.data || `${error.response.status}: ${error.response.statusText}` + : error.message enqueueSnackbar({ message: `Failed to save API Configuration: ${errorData}`, options: { From c4c007b16402c32a39c2c483eef550aa21b1625d Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 9 Sep 2023 23:25:28 +0800 Subject: [PATCH 8/9] add migration queries for rate limit --- packages/server/src/database/entities/ChatFlow.ts | 2 +- .../migrations/mysql/1694099200729-AddApiConfig.ts | 11 +++++++++++ .../server/src/database/migrations/mysql/index.ts | 4 +++- .../migrations/postgres/1694099183389-AddApiConfig.ts | 11 +++++++++++ .../server/src/database/migrations/postgres/index.ts | 4 +++- .../migrations/sqlite/1694090982460-AddApiConfig.ts | 11 +++++++++++ .../server/src/database/migrations/sqlite/index.ts | 4 +++- packages/ui/src/views/chatflows/Configuration.js | 6 +++--- 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts create mode 100644 packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts create mode 100644 packages/server/src/database/migrations/sqlite/1694090982460-AddApiConfig.ts diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 6f66d694..29f58e5c 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -25,7 +25,7 @@ export class ChatFlow implements IChatFlow { @Column({ nullable: true, type: 'text' }) chatbotConfig?: string - @Column({ nullable: true }) + @Column({ nullable: true, type: 'text' }) apiConfig?: string @CreateDateColumn() diff --git a/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts b/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts new file mode 100644 index 00000000..c82b36ea --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1694099200729-AddApiConfig.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiConfig1694099200729 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`apiConfig\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`apiConfig\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 124eb70f..e5c16773 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -3,11 +3,13 @@ import { ModifyChatFlow1693997791471 } from './1693997791471-ModifyChatFlow' import { ModifyChatMessage1693999022236 } from './1693999022236-ModifyChatMessage' import { ModifyCredential1693999261583 } from './1693999261583-ModifyCredential' import { ModifyTool1694001465232 } from './1694001465232-ModifyTool' +import { AddApiConfig1694099200729 } from './1694099200729-AddApiConfig' export const mysqlMigrations = [ Init1693840429259, ModifyChatFlow1693997791471, ModifyChatMessage1693999022236, ModifyCredential1693999261583, - ModifyTool1694001465232 + ModifyTool1694001465232, + AddApiConfig1694099200729 ] diff --git a/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts b/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts new file mode 100644 index 00000000..832c2fa3 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1694099183389-AddApiConfig.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiConfig1694099183389 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "apiConfig" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "apiConfig";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 2bac9f33..04579cd6 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -3,11 +3,13 @@ import { ModifyChatFlow1693995626941 } from './1693995626941-ModifyChatFlow' import { ModifyChatMessage1693996694528 } from './1693996694528-ModifyChatMessage' import { ModifyCredential1693997070000 } from './1693997070000-ModifyCredential' import { ModifyTool1693997339912 } from './1693997339912-ModifyTool' +import { AddApiConfig1694099183389 } from './1694099183389-AddApiConfig' export const postgresMigrations = [ Init1693891895163, ModifyChatFlow1693995626941, ModifyChatMessage1693996694528, ModifyCredential1693997070000, - ModifyTool1693997339912 + ModifyTool1693997339912, + AddApiConfig1694099183389 ] diff --git a/packages/server/src/database/migrations/sqlite/1694090982460-AddApiConfig.ts b/packages/server/src/database/migrations/sqlite/1694090982460-AddApiConfig.ts new file mode 100644 index 00000000..6bdff60f --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1694090982460-AddApiConfig.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddApiConfig1694090982460 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "apiConfig" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "apiConfig";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index c3c1fe7a..234dd220 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -3,11 +3,13 @@ import { ModifyChatFlow1693920824108 } from './1693920824108-ModifyChatFlow' import { ModifyChatMessage1693921865247 } from './1693921865247-ModifyChatMessage' import { ModifyCredential1693923551694 } from './1693923551694-ModifyCredential' import { ModifyTool1693924207475 } from './1693924207475-ModifyTool' +import { AddApiConfig1694090982460 } from './1694090982460-AddApiConfig' export const sqliteMigrations = [ Init1693835579790, ModifyChatFlow1693920824108, ModifyChatMessage1693921865247, ModifyCredential1693923551694, - ModifyTool1693924207475 + ModifyTool1693924207475, + AddApiConfig1694090982460 ] diff --git a/packages/ui/src/views/chatflows/Configuration.js b/packages/ui/src/views/chatflows/Configuration.js index 4268fd08..51826c44 100644 --- a/packages/ui/src/views/chatflows/Configuration.js +++ b/packages/ui/src/views/chatflows/Configuration.js @@ -28,9 +28,9 @@ const Configuration = () => { const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [limitMax, setLimitMax] = useState(apiConfig?.rateLimit?.limitMax ?? 20) - const [limitDuration, setLimitDuration] = useState(apiConfig?.rateLimit?.limitDuration ?? 120) - const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? "Please don't spam me") + const [limitMax, setLimitMax] = useState(apiConfig?.rateLimit?.limitMax ?? '') + const [limitDuration, setLimitDuration] = useState(apiConfig?.rateLimit?.limitDuration ?? '') + const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? '') const formatObj = () => { const obj = { From c4243d6bd1b71d1f8bee05daf2b8a49d74f9d4f9 Mon Sep 17 00:00:00 2001 From: chungyau97 Date: Sat, 9 Sep 2023 23:26:08 +0800 Subject: [PATCH 9/9] add hosting proxy configuration --- docker/.env.example | 2 ++ packages/server/.env.example | 2 ++ packages/server/src/index.ts | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index 16b19cdc..bee2dfbf 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -5,6 +5,8 @@ APIKEY_PATH=/root/.flowise SECRETKEY_PATH=/root/.flowise LOG_PATH=/root/.flowise/logs +# NUMBER_OF_PROXIES= 1 + # DATABASE_TYPE=postgres # DATABASE_PORT="" # DATABASE_HOST="" diff --git a/packages/server/.env.example b/packages/server/.env.example index bedbf638..07dbf6b6 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -5,6 +5,8 @@ PASSPHRASE=MYPASSPHRASE # Passphrase used to create encryption key # SECRETKEY_PATH=/your_api_key_path/.flowise # LOG_PATH=/your_log_path/.flowise/logs +# NUMBER_OF_PROXIES= 1 + # DATABASE_TYPE=postgres # DATABASE_PORT="" # DATABASE_HOST="" diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7143bdc5..8d83cb8a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -102,6 +102,9 @@ export class App { this.app.use(express.json({ limit: '50mb' })) this.app.use(express.urlencoded({ limit: '50mb', extended: true })) + if (process.env.NUMBER_OF_PROXIES && parseInt(process.env.NUMBER_OF_PROXIES) > 0) + this.app.set('trust proxy', parseInt(process.env.NUMBER_OF_PROXIES)) + // Allow access from * this.app.use(cors()) @@ -121,7 +124,8 @@ export class App { '/api/v1/prediction/', '/api/v1/node-icon/', '/api/v1/components-credentials-icon/', - '/api/v1/chatflows-streaming' + '/api/v1/chatflows-streaming', + '/api/v1/ip' ] this.app.use((req, res, next) => { if (req.url.includes('/api/v1/')) { @@ -132,6 +136,16 @@ export class App { const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` }) + // ---------------------------------------- + // Configure number of proxies in Host Environment + // ---------------------------------------- + this.app.get('/api/v1/ip', (request, response) => { + response.send({ + ip: request.ip, + msg: 'See the IP returned in the response. If it matches your IP address (which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/), then the number of proxies is correct and the rate limiter should now work correctly. If not, then keep increasing the number until it does.' + }) + }) + // ---------------------------------------- // Components // ----------------------------------------