Skip to content

Commit 7b5e7b5

Browse files
committed
Apply PR aliasrobotics#388: add rate limiting to web logs endpoints
1 parent eb2a4b4 commit 7b5e7b5

File tree

1 file changed

+74
-2
lines changed

1 file changed

+74
-2
lines changed

tools/logs.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,86 @@
1919
import matplotlib
2020
matplotlib.use('Agg')
2121

22-
from flask import Flask, render_template
22+
from flask import Flask, render_template, request
23+
from functools import wraps
2324
import pandas as pd
2425
import matplotlib.pyplot as plt
2526
import io
2627
import base64
27-
from datetime import datetime
28+
from datetime import datetime, timedelta
2829
import os
2930
import folium
3031
import requests
3132
import argparse
3233
from typing import Dict, Optional
3334
import numpy as np
3435
import re
36+
from collections import defaultdict
37+
import time
3538

3639
app = Flask(__name__)
3740

41+
# Rate limiting configuration
42+
class RateLimiter:
43+
"""In-memory rate limiter to prevent DoS attacks on API endpoints."""
44+
45+
def __init__(self, requests_per_minute: int = 10, requests_per_hour: int = 50, requests_per_day: int = 200):
46+
self.requests_per_minute = requests_per_minute
47+
self.requests_per_hour = requests_per_hour
48+
self.requests_per_day = requests_per_day
49+
self.request_history: Dict[str, list] = defaultdict(list)
50+
51+
def is_rate_limited(self, client_id: str) -> bool:
52+
"""Return True if the client has exceeded configured rate limits."""
53+
current_time = time.time()
54+
55+
# Clean up requests older than 24 hours
56+
self.request_history[client_id] = [
57+
req_time
58+
for req_time in self.request_history[client_id]
59+
if current_time - req_time < 86400
60+
]
61+
62+
# Minute window
63+
minute_requests = [r for r in self.request_history[client_id] if current_time - r < 60]
64+
if len(minute_requests) >= self.requests_per_minute:
65+
return True
66+
67+
# Hour window
68+
hour_requests = [r for r in self.request_history[client_id] if current_time - r < 3600]
69+
if len(hour_requests) >= self.requests_per_hour:
70+
return True
71+
72+
# Day window
73+
day_requests = [r for r in self.request_history[client_id] if current_time - r < 86400]
74+
if len(day_requests) >= self.requests_per_day:
75+
return True
76+
77+
# Record this request and allow
78+
self.request_history[client_id].append(current_time)
79+
return False
80+
81+
82+
# Initialize the global rate limiter
83+
rate_limiter = RateLimiter(requests_per_minute=10, requests_per_hour=50, requests_per_day=200)
84+
85+
86+
def apply_rate_limit():
87+
"""Check request and return a Flask-style response tuple on rate limit, else None."""
88+
try:
89+
# Prefer X-Forwarded-For when behind proxies
90+
forwarded = request.headers.get('X-Forwarded-For', None)
91+
if forwarded:
92+
client_ip = forwarded.split(',')[0].strip()
93+
else:
94+
client_ip = request.remote_addr or 'unknown'
95+
except Exception:
96+
client_ip = 'unknown'
97+
98+
if rate_limiter.is_rate_limited(client_ip):
99+
return "Rate limit exceeded. Please try again later.", 429
100+
return None
101+
38102
# Configuration for enabled visualizations
39103
class Config:
40104
def __init__(self):
@@ -508,6 +572,10 @@ def create_pypi_plot():
508572

509573
@app.route('/')
510574
def index():
575+
# Apply rate limiting
576+
rate_limit_response = apply_rate_limit()
577+
if rate_limit_response:
578+
return rate_limit_response
511579
# Get log file path from app config
512580
log_file = app.config['LOG_FILE']
513581

@@ -544,6 +612,10 @@ def index():
544612

545613
@app.route('/pypi-stats')
546614
def pypi_stats():
615+
# Apply rate limiting
616+
rate_limit_response = apply_rate_limit()
617+
if rate_limit_response:
618+
return rate_limit_response
547619
# Generate PyPI plot
548620
pypi_plot, stats = create_pypi_plot()
549621

0 commit comments

Comments
 (0)