import re from datetime import datetime from typing import Optional, Tuple import html from functools import wraps import time import gradio as gr import logging from core.logging_config import get_logger # Initialize logger logger = get_logger(__name__) def validate_datetime_format(datetime_str: str) -> Tuple[bool, Optional[datetime], Optional[str]]: """ Validates datetime string format and returns parsed datetime object Expected format: DD-MM-YYYY HH:MM:SS """ if not datetime_str or not isinstance(datetime_str, str): return False, None, "Datetime string is empty or not a string" # Check for valid format using regex pattern = r'^(\d{2})-(\d{2})-(\d{4})\s(\d{2}):(\d{2}):(\d{2})$' match = re.match(pattern, datetime_str.strip()) if not match: return False, None, f"Datetime format invalid. Expected DD-MM-YYYY HH:MM:SS, got: {datetime_str}" try: day, month, year, hour, minute, second = map(int, match.groups()) # Validate date components if not (1 <= day <= 31): return False, None, f"Invalid day: {day}. Day must be between 1 and 31" if not (1 <= month <= 12): return False, None, f"Invalid month: {month}. Month must be between 1 and 12" if year < 1900 or year > 2100: return False, None, f"Invalid year: {year}. Year must be between 1900 and 2100" if not (0 <= hour <= 23): return False, None, f"Invalid hour: {hour}. Hour must be between 0 and 23" if not (0 <= minute <= 59): return False, None, f"Invalid minute: {minute}. Minute must be between 0 and 59" if not (0 <= second <= 59): return False, None, f"Invalid second: {second}. Second must be between 0 and 59" # Try to create datetime object to catch invalid dates like 30th of February dt = datetime(year, month, day, hour, minute, second) return True, dt, None except ValueError as e: return False, None, f"Invalid date: {str(e)}" except Exception as e: return False, None, f"Error parsing datetime: {str(e)}" def validate_city_name(city: str) -> Tuple[bool, Optional[str], Optional[str]]: """ Validates city name input to prevent injection attacks """ if not city or not isinstance(city, str): return False, None, "City name is empty or not a string" # Sanitize input by removing potentially dangerous characters sanitized_city = html.escape(city.strip()) # Check for potentially dangerous patterns dangerous_patterns = [ r'[<>"\']', # HTML/SQL injection characters r'[;{}]', # Command injection characters r'\$\(', # Command substitution r'`', # Command execution ] for pattern in dangerous_patterns: if re.search(pattern, sanitized_city): return False, None, f"Potentially dangerous characters detected in city name: {sanitized_city}" # Additional check for length if len(sanitized_city) > 100: return False, None, f"City name too long: {len(sanitized_city)} characters. Maximum is 100" # Check if it contains only alphanumeric characters, spaces, hyphens, commas, slashes, periods, and cyrillic if not re.match(r'^[a-zA-Zа-яА-ЯёЁ0-9\s\-\/\.,]{2,100}$', sanitized_city): return False, None, f"City name contains invalid characters: {sanitized_city}" return True, sanitized_city, None def validate_coordinates(lat: float, lon: float) -> Tuple[bool, Optional[str]]: """ Validates geographic coordinates """ if not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)): return False, "Coordinates must be numeric values" if not (-90 <= lat <= 90): return False, f"Latitude out of range: {lat}. Must be between -90 and 90" if not (-180 <= lon <= 180): return False, f"Longitude out of range: {lon}. Must be between -180 and 180" return True, None def sanitize_input(input_str: str) -> str: """ Sanitizes string input by removing potentially dangerous characters """ if not input_str or not isinstance(input_str, str): return "" # Remove potentially dangerous characters sanitized = html.escape(input_str.strip()) # Remove any control characters sanitized = ''.join(char for char in sanitized if ord(char) >= 32 or char in ['\n', '\t', '\r']) return sanitized def sanitize_inputs(datetime_str: str, city: str) -> tuple[bool, str]: """Validate and sanitize user inputs""" # Validate datetime format if not re.match(r'^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$', datetime_str): return False, "Invalid datetime format" # Validate city name (alphanumeric, spaces, hyphens, commas, slashes, periods, and cyrillic) if not re.match(r'^[a-zA-Zа-яА-ЯёЁ0-9\s\-\/\.,]{2,100}$', city): return False, "Invalid city name" # Additional date validation try: day, month, year_time = datetime_str.split('-') year, time = year_time.split(' ') datetime(int(year), int(month), int(day)) except ValueError: return False, "Invalid date" return True, "" class RateLimiter: def __init__(self, max_calls: int, period: int): self.max_calls = max_calls self.period = period self.calls = [] def __call__(self, func): @wraps(func) def wrapper(*args, **kwargs): now = time.time() # Remove calls outside the current period self.calls = [call for call in self.calls if now - call < self.period] if len(self.calls) >= self.max_calls: logger.warning(f"Rate limit exceeded for function {func.__name__}") raise gr.Error("Rate limit exceeded. Please try again later.") self.calls.append(now) logger.debug(f"Function {func.__name__} called, current call count: {len(self.calls)}") return func(*args, **kwargs) return wrapper