OhMyDitzzy commited on
Commit
1dbfa1e
·
1 Parent(s): 496dca4

feat: rate limit

Browse files
src/lib/api-url.ts CHANGED
@@ -4,11 +4,11 @@ export function getBaseUrl(): string {
4
  /*if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
5
  return process.env.DOMAIN_URL;
6
  }*/
7
- return `${window.location.protocol}//${window.location.host}`;
8
  }
9
 
10
  return process.env.NODE_ENV === 'production'
11
- ? `${window.location.protocol}//${window.location.host}`
12
  : 'http://localhost:5000';
13
  }
14
 
 
4
  /*if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
5
  return process.env.DOMAIN_URL;
6
  }*/
7
+ return `${(window as any).location.protocol}//${(window as any).location.host}`;
8
  }
9
 
10
  return process.env.NODE_ENV === 'production'
11
+ ? `${(window as any).location.protocol}//${(window as any).location.host}`
12
  : 'http://localhost:5000';
13
  }
14
 
src/server/index.ts CHANGED
@@ -101,7 +101,7 @@ app.use((req, res, next) => {
101
  let capturedJsonResponse: Record<string, any> | undefined = undefined;
102
 
103
  const originalResJson = res.json;
104
- res.json = function (bodyJson, ...args) {
105
  capturedJsonResponse = bodyJson;
106
  return originalResJson.apply(res, [bodyJson, ...args]);
107
  };
@@ -115,19 +115,23 @@ app.use((req, res, next) => {
115
  }
116
 
117
  log(logLine);
118
-
119
  const excludedPaths = [
120
  '/api/plugins',
121
- '/api/stats',
122
  '/api/categories',
123
  '/docs'
124
  ];
125
-
126
  const isPluginEndpoint = !excludedPaths.some(excluded => path.startsWith(excluded));
127
-
128
  if (isPluginEndpoint) {
129
  const clientIp = req.ip || req.socket.remoteAddress || "unknown";
130
- getStatsTracker().trackRequest(path, res.statusCode, clientIp);
 
 
 
 
131
  }
132
  }
133
  });
@@ -138,14 +142,14 @@ app.use((req, res, next) => {
138
  (async () => {
139
  initStatsTracker();
140
  log("Stats tracker initialized");
141
-
142
  const pluginsDir = join(process.cwd(), "src/server/plugins");
143
  const pluginLoader = initPluginLoader(pluginsDir);
144
-
145
  const isDev = process.env.NODE_ENV === "development";
146
  await pluginLoader.loadPlugins(app, isDev);
147
 
148
- app.get("/api/plugins", (req, res) => {
149
  const metadata = getPluginLoader().getPluginMetadata();
150
  res.json({
151
  success: true,
@@ -157,10 +161,10 @@ app.use((req, res, next) => {
157
  app.get("/api/plugins/category/:category", (req, res) => {
158
  const { category } = req.params;
159
  const allPlugins = getPluginLoader().getPluginMetadata();
160
- const filtered = allPlugins.filter(p =>
161
  p.category.includes(category)
162
  );
163
-
164
  res.json({
165
  success: true,
166
  category,
@@ -168,11 +172,11 @@ app.use((req, res, next) => {
168
  plugins: filtered,
169
  });
170
  });
171
-
172
- app.get("/api/stats", (req, res) => {
173
  const globalStats = getStatsTracker().getGlobalStats();
174
  const topEndpoints = getStatsTracker().getTopEndpoints(5);
175
-
176
  res.json({
177
  success: true,
178
  stats: {
@@ -181,17 +185,17 @@ app.use((req, res, next) => {
181
  },
182
  });
183
  });
184
-
185
- app.get("/api/stats/visitors", (req, res) => {
186
  const chartData = getStatsTracker().getVisitorChartData();
187
-
188
  res.json({
189
  success: true,
190
  data: chartData,
191
  });
192
  });
193
-
194
- app.get("/api/categories", (req, res) => {
195
  const allPlugins = getPluginLoader().getPluginMetadata();
196
  const categoriesMap = new Map<string, number>();
197
 
@@ -210,7 +214,7 @@ app.use((req, res, next) => {
210
  success: true,
211
  categories,
212
  });
213
- });
214
 
215
  app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
216
  const status = err.status || err.statusCode || 500;
@@ -226,7 +230,7 @@ app.use((req, res, next) => {
226
  const { setupVite } = await import("./vite");
227
  await setupVite(httpServer, app);
228
  }
229
-
230
  app.use((req: Request, res: Response, next: NextFunction) => {
231
  if (req.path.startsWith("/api")) {
232
  return res.status(404).json({
@@ -248,7 +252,7 @@ app.use((req, res, next) => {
248
  log(`serving on port ${port}`);
249
  },
250
  );
251
-
252
  process.on('uncaughtException', (error: Error) => {
253
  log(`Uncaught Exception: ${error.message}`, 'error');
254
  console.error(error.stack);
@@ -258,4 +262,4 @@ app.use((req, res, next) => {
258
  log(`Unhandled Rejection at: ${promise}, reason: ${reason}`, 'error');
259
  console.error(reason);
260
  });
261
- })();
 
101
  let capturedJsonResponse: Record<string, any> | undefined = undefined;
102
 
103
  const originalResJson = res.json;
104
+ res.json = function(bodyJson, ...args) {
105
  capturedJsonResponse = bodyJson;
106
  return originalResJson.apply(res, [bodyJson, ...args]);
107
  };
 
115
  }
116
 
117
  log(logLine);
118
+
119
  const excludedPaths = [
120
  '/api/plugins',
121
+ '/api/stats',
122
  '/api/categories',
123
  '/docs'
124
  ];
125
+
126
  const isPluginEndpoint = !excludedPaths.some(excluded => path.startsWith(excluded));
127
+
128
  if (isPluginEndpoint) {
129
  const clientIp = req.ip || req.socket.remoteAddress || "unknown";
130
+ const tracked = getStatsTracker().trackRequest(path, res.statusCode, clientIp);
131
+
132
+ if (!tracked) {
133
+ log(`Failed request from ${clientIp} not tracked (limit exceeded)`, "stats");
134
+ }
135
  }
136
  }
137
  });
 
142
  (async () => {
143
  initStatsTracker();
144
  log("Stats tracker initialized");
145
+
146
  const pluginsDir = join(process.cwd(), "src/server/plugins");
147
  const pluginLoader = initPluginLoader(pluginsDir);
148
+
149
  const isDev = process.env.NODE_ENV === "development";
150
  await pluginLoader.loadPlugins(app, isDev);
151
 
152
+ app.get("/api/plugins", (_req, res) => {
153
  const metadata = getPluginLoader().getPluginMetadata();
154
  res.json({
155
  success: true,
 
161
  app.get("/api/plugins/category/:category", (req, res) => {
162
  const { category } = req.params;
163
  const allPlugins = getPluginLoader().getPluginMetadata();
164
+ const filtered = allPlugins.filter(p =>
165
  p.category.includes(category)
166
  );
167
+
168
  res.json({
169
  success: true,
170
  category,
 
172
  plugins: filtered,
173
  });
174
  });
175
+
176
+ app.get("/api/stats", (_req, res) => {
177
  const globalStats = getStatsTracker().getGlobalStats();
178
  const topEndpoints = getStatsTracker().getTopEndpoints(5);
179
+
180
  res.json({
181
  success: true,
182
  stats: {
 
185
  },
186
  });
187
  });
188
+
189
+ app.get("/api/stats/visitors", (_req, res) => {
190
  const chartData = getStatsTracker().getVisitorChartData();
191
+
192
  res.json({
193
  success: true,
194
  data: chartData,
195
  });
196
  });
197
+
198
+ app.get("/api/categories", (_req, res) => {
199
  const allPlugins = getPluginLoader().getPluginMetadata();
200
  const categoriesMap = new Map<string, number>();
201
 
 
214
  success: true,
215
  categories,
216
  });
217
+ });
218
 
219
  app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
220
  const status = err.status || err.statusCode || 500;
 
230
  const { setupVite } = await import("./vite");
231
  await setupVite(httpServer, app);
232
  }
233
+
234
  app.use((req: Request, res: Response, next: NextFunction) => {
235
  if (req.path.startsWith("/api")) {
236
  return res.status(404).json({
 
252
  log(`serving on port ${port}`);
253
  },
254
  );
255
+
256
  process.on('uncaughtException', (error: Error) => {
257
  log(`Uncaught Exception: ${error.message}`, 'error');
258
  console.error(error.stack);
 
262
  log(`Unhandled Rejection at: ${promise}, reason: ${reason}`, 'error');
263
  console.error(reason);
264
  });
265
+ })();
src/server/lib/stats-tracker.ts CHANGED
@@ -10,6 +10,11 @@ interface VisitorData {
10
  count: number;
11
  }
12
 
 
 
 
 
 
13
  interface GlobalStats {
14
  totalRequests: number;
15
  totalSuccess: number;
@@ -22,6 +27,9 @@ interface GlobalStats {
22
 
23
  class StatsTracker {
24
  private stats: GlobalStats;
 
 
 
25
 
26
  constructor() {
27
  this.stats = {
@@ -33,13 +41,52 @@ class StatsTracker {
33
  startTime: Date.now(),
34
  visitorsByHour: new Map(),
35
  };
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
- trackRequest(endpoint: string, statusCode: number, clientIp: string) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  this.stats.totalRequests++;
40
  this.stats.uniqueVisitors.add(clientIp);
41
 
42
- const currentHour = Math.floor(Date.now() / (1000 * 60 * 60));
43
  if (!this.stats.visitorsByHour.has(currentHour)) {
44
  this.stats.visitorsByHour.set(currentHour, new Set());
45
  }
@@ -63,19 +110,21 @@ class StatsTracker {
63
  totalRequests: 0,
64
  successRequests: 0,
65
  failedRequests: 0,
66
- lastAccessed: Date.now(),
67
  });
68
  }
69
 
70
  const endpointStats = this.stats.endpoints.get(endpoint)!;
71
  endpointStats.totalRequests++;
72
- endpointStats.lastAccessed = Date.now();
73
 
74
  if (statusCode >= 200 && statusCode < 400) {
75
  endpointStats.successRequests++;
76
  } else {
77
  endpointStats.failedRequests++;
78
  }
 
 
79
  }
80
 
81
  getGlobalStats() {
@@ -149,6 +198,7 @@ class StatsTracker {
149
  startTime: Date.now(),
150
  visitorsByHour: new Map(),
151
  };
 
152
  }
153
  }
154
 
 
10
  count: number;
11
  }
12
 
13
+ interface IPFailureTracking {
14
+ count: number;
15
+ resetTime: number;
16
+ }
17
+
18
  interface GlobalStats {
19
  totalRequests: number;
20
  totalSuccess: number;
 
27
 
28
  class StatsTracker {
29
  private stats: GlobalStats;
30
+ private ipFailures: Map<string, IPFailureTracking>;
31
+ private readonly MAX_FAILS_PER_IP = 1;
32
+ private readonly FAIL_WINDOW_MS = 12 * 60 * 60 * 1000;
33
 
34
  constructor() {
35
  this.stats = {
 
41
  startTime: Date.now(),
42
  visitorsByHour: new Map(),
43
  };
44
+ this.ipFailures = new Map();
45
+
46
+ setInterval(() => {
47
+ const now = Date.now();
48
+ this.ipFailures.forEach((tracking, ip) => {
49
+ if (now > tracking.resetTime) {
50
+ this.ipFailures.delete(ip);
51
+ }
52
+ });
53
+ }, 5 * 60 * 1000);
54
  }
55
 
56
+ trackRequest(endpoint: string, statusCode: number, clientIp: string): boolean {
57
+ const now = Date.now();
58
+ const isFailed = statusCode >= 400;
59
+
60
+ if (isFailed) {
61
+ const ipTracking = this.ipFailures.get(clientIp);
62
+
63
+ if (!ipTracking) {
64
+ this.ipFailures.set(clientIp, {
65
+ count: 1,
66
+ resetTime: now + this.FAIL_WINDOW_MS,
67
+ });
68
+ } else {
69
+ if (now > ipTracking.resetTime) {
70
+ ipTracking.count = 1;
71
+ ipTracking.resetTime = now + this.FAIL_WINDOW_MS;
72
+ } else {
73
+ if (ipTracking.count >= this.MAX_FAILS_PER_IP) {
74
+ return false;
75
+ }
76
+ ipTracking.count++;
77
+ }
78
+ }
79
+ } else {
80
+ const ipTracking = this.ipFailures.get(clientIp);
81
+ if (ipTracking && ipTracking.count > 0) {
82
+ ipTracking.count--;
83
+ }
84
+ }
85
+
86
  this.stats.totalRequests++;
87
  this.stats.uniqueVisitors.add(clientIp);
88
 
89
+ const currentHour = Math.floor(now / (1000 * 60 * 60));
90
  if (!this.stats.visitorsByHour.has(currentHour)) {
91
  this.stats.visitorsByHour.set(currentHour, new Set());
92
  }
 
110
  totalRequests: 0,
111
  successRequests: 0,
112
  failedRequests: 0,
113
+ lastAccessed: now,
114
  });
115
  }
116
 
117
  const endpointStats = this.stats.endpoints.get(endpoint)!;
118
  endpointStats.totalRequests++;
119
+ endpointStats.lastAccessed = now;
120
 
121
  if (statusCode >= 200 && statusCode < 400) {
122
  endpointStats.successRequests++;
123
  } else {
124
  endpointStats.failedRequests++;
125
  }
126
+
127
+ return true;
128
  }
129
 
130
  getGlobalStats() {
 
198
  startTime: Date.now(),
199
  visitorsByHour: new Map(),
200
  };
201
+ this.ipFailures.clear();
202
  }
203
  }
204
 
src/server/plugins/data.js CHANGED
@@ -7,7 +7,7 @@ const handler = {
7
  exec: async (req, res) => {
8
  res.json({
9
  status: 200,
10
- message: "Welcome to DitzzyAPI, Lets get started by visit our documentation on: https://ditzzy-api.hf.space/docs"
11
  })
12
  }
13
  }
 
7
  exec: async (req, res) => {
8
  res.json({
9
  status: 200,
10
+ message: "Welcome to DitzzyAPI, Lets get started by visit our documentation on: https://api.ditzzy.my.id/docs"
11
  })
12
  }
13
  }