|
19 | 19 | import matplotlib |
20 | 20 | matplotlib.use('Agg') |
21 | 21 |
|
22 | | -from flask import Flask, render_template |
| 22 | +from flask import Flask, render_template, request |
| 23 | +from functools import wraps |
23 | 24 | import pandas as pd |
24 | 25 | import matplotlib.pyplot as plt |
25 | 26 | import io |
26 | 27 | import base64 |
27 | | -from datetime import datetime |
| 28 | +from datetime import datetime, timedelta |
28 | 29 | import os |
29 | 30 | import folium |
30 | 31 | import requests |
31 | 32 | import argparse |
32 | 33 | from typing import Dict, Optional |
33 | 34 | import numpy as np |
34 | 35 | import re |
| 36 | +from collections import defaultdict |
| 37 | +import time |
35 | 38 |
|
36 | 39 | app = Flask(__name__) |
37 | 40 |
|
| 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 | + |
38 | 102 | # Configuration for enabled visualizations |
39 | 103 | class Config: |
40 | 104 | def __init__(self): |
@@ -508,6 +572,10 @@ def create_pypi_plot(): |
508 | 572 |
|
509 | 573 | @app.route('/') |
510 | 574 | def index(): |
| 575 | + # Apply rate limiting |
| 576 | + rate_limit_response = apply_rate_limit() |
| 577 | + if rate_limit_response: |
| 578 | + return rate_limit_response |
511 | 579 | # Get log file path from app config |
512 | 580 | log_file = app.config['LOG_FILE'] |
513 | 581 |
|
@@ -544,6 +612,10 @@ def index(): |
544 | 612 |
|
545 | 613 | @app.route('/pypi-stats') |
546 | 614 | def pypi_stats(): |
| 615 | + # Apply rate limiting |
| 616 | + rate_limit_response = apply_rate_limit() |
| 617 | + if rate_limit_response: |
| 618 | + return rate_limit_response |
547 | 619 | # Generate PyPI plot |
548 | 620 | pypi_plot, stats = create_pypi_plot() |
549 | 621 |
|
|
0 commit comments