| |
| """ |
| 请求处理相关的辅助函数。 |
| 包含获取客户端 IP、基于 IP 的速率限制(可能已废弃)以及获取时间戳的功能。 |
| """ |
| import time |
| import logging |
| from datetime import datetime, timedelta, timezone |
| import pytz |
| from fastapi import Request, HTTPException |
| from collections import defaultdict |
| from threading import Lock |
| from typing import Dict, Union, List, Tuple |
|
|
| |
| logger = logging.getLogger("my_logger") |
|
|
| |
|
|
| |
| |
| ip_request_data: Dict[str, Dict[str, Union[int, List[float]]]] = defaultdict(lambda: {"daily_count": 0, "timestamps": []}) |
| |
| ip_daily_counts_lock = Lock() |
|
|
| |
| pacific_tz = pytz.timezone('America/Los_Angeles') |
|
|
| def get_client_ip(request: Request) -> str: |
| """ |
| 从 FastAPI 请求对象中提取真实的客户端 IP 地址。 |
| 优先检查常见的代理头 ('X-Forwarded-For', 'X-Real-IP'), |
| 如果不存在,则回退到直接连接的客户端地址 (`request.client.host`)。 |
| |
| Args: |
| request (Request): FastAPI 请求对象。 |
| |
| Returns: |
| str: 客户端 IP 地址字符串。如果无法确定,则返回 "Unknown"。 |
| """ |
| |
| x_forwarded_for = request.headers.get("x-forwarded-for") |
| if x_forwarded_for: |
| |
| ip = x_forwarded_for.split(",")[0].strip() |
| logger.debug(f"从 X-Forwarded-For 获取 IP: {ip}") |
| return ip |
| |
| x_real_ip = request.headers.get("x-real-ip") |
| if x_real_ip: |
| logger.debug(f"从 X-Real-IP 获取 IP: {x_real_ip}") |
| return x_real_ip |
| |
| |
| client_host = request.client.host if request.client else None |
| if client_host: |
| logger.debug(f"从 request.client.host 获取 IP: {client_host}") |
| return client_host |
| else: |
| logger.warning("无法从请求头或 client 属性中获取客户端 IP 地址。") |
| return "Unknown" |
|
|
|
|
| def protect_from_abuse(request: Request, max_rpm: int, max_rpd: int): |
| """ |
| (可能已废弃/需要审查) 基于 IP 地址实现简单的请求速率 (RPM) 和每日总量 (RPD) 限制。 |
| 注意:此函数使用全局字典 `ip_request_data` 存储状态,可能与基于 Key 的限制冲突或重复。 |
| 在当前系统中,速率限制主要由 Key Manager 处理,此函数可能不再需要或需要重新设计。 |
| |
| Args: |
| request (Request): FastAPI 请求对象。 |
| max_rpm (int): 每个 IP 允许的最大每分钟请求数。 |
| max_rpd (int): 每个 IP 允许的最大每日请求数。 |
| |
| Raises: |
| HTTPException (429 Too Many Requests): 如果检测到超过速率或每日限制。 |
| """ |
| logger.warning("调用了可能已废弃的 protect_from_abuse 函数 (基于 IP 的速率限制)。") |
| global ip_request_data, ip_daily_counts_lock, pacific_tz |
| ip = get_client_ip(request) |
| if ip == "Unknown": |
| logger.warning("无法获取客户端 IP 地址,跳过滥用检查。") |
| return |
|
|
| now = time.time() |
| today_pacific = datetime.now(pacific_tz).date() |
|
|
| with ip_daily_counts_lock: |
| ip_data = ip_request_data[ip] |
| daily_count = ip_data.get("daily_count", 0) |
| timestamps: List[float] = ip_data.get("timestamps", []) |
|
|
| |
| |
| last_request_date_pacific = datetime.fromtimestamp(timestamps[0], pacific_tz).date() if timestamps else None |
| |
| if last_request_date_pacific != today_pacific: |
| daily_count = 0 |
| timestamps = [] |
| ip_data["daily_count"] = 0 |
| ip_data["timestamps"] = [] |
| logger.debug(f"IP {ip} 的每日计数已重置 (新的一天)。") |
|
|
| |
| if daily_count >= max_rpd: |
| logger.warning(f"IP {ip} 已达到每日请求限制 ({max_rpd} RPD)。") |
| |
| raise HTTPException(status_code=429, detail=f"您已达到每日请求限制 ({max_rpd} RPD)。请明天再试。") |
|
|
| |
| rpm_window_seconds = 60 |
| |
| timestamps = [ts for ts in timestamps if now - ts < rpm_window_seconds] |
| |
| if len(timestamps) >= max_rpm: |
| |
| earliest_timestamp = timestamps[0] if timestamps else now |
| wait_time = max(0.0, earliest_timestamp + rpm_window_seconds - now) |
| logger.warning(f"IP {ip} 请求过于频繁,触发 RPM 限制 ({max_rpm} RPM)。需要等待 {wait_time:.2f} 秒。") |
| |
| raise HTTPException(status_code=429, detail=f"请求过于频繁。请在 {wait_time:.2f} 秒后重试。") |
|
|
| |
| |
| ip_data["daily_count"] = daily_count + 1 |
| timestamps.append(now) |
| ip_data["timestamps"] = timestamps |
| logger.debug(f"IP {ip} 请求计数更新: RPD={ip_data['daily_count']}, RPM_Window_Count={len(timestamps)}") |
|
|
| def get_current_timestamps() -> Tuple[float, str]: |
| """ |
| 获取当前的 Unix 时间戳和太平洋时区的日期字符串。 |
| |
| Returns: |
| Tuple[float, str]: |
| - 第一个元素:当前的 Unix 时间戳 (float)。 |
| - 第二个元素:太平洋时区的当前日期字符串 (str, 'YYYY-MM-DD' 格式)。 |
| """ |
| now_timestamp = time.time() |
| |
| today_pacific = datetime.now(pacific_tz).date() |
| today_date_str_pt = today_pacific.isoformat() |
|
|
| return now_timestamp, today_date_str_pt |
|
|