File size: 6,232 Bytes
790625d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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