191 lines
5.6 KiB
Python
191 lines
5.6 KiB
Python
import time
|
|
import hashlib
|
|
import json
|
|
from collections import defaultdict
|
|
from flask import request, render_template
|
|
import redis
|
|
import os
|
|
import database
|
|
|
|
redis_client = redis.Redis(
|
|
host=os.environ.get('REDIS_HOST', 'localhost'),
|
|
port=int(os.environ.get('REDIS_PORT', 6379)),
|
|
db=int(os.environ.get('REDIS_DB', 0)),
|
|
password=os.environ.get('REDIS_PASSWORD', None),
|
|
decode_responses=True
|
|
)
|
|
|
|
BLACKLIST_PREFIX = 'blacklist:'
|
|
ATTEMPTS_PREFIX = 'attempts:'
|
|
BLOCKED_PREFIX = 'blocked:'
|
|
|
|
SECURITY_CONFIG = {
|
|
'max_attempts': 5,
|
|
'block_duration': 300,
|
|
'block_duration_increment': 60,
|
|
'max_block_duration': 3600,
|
|
'attempt_window': 60,
|
|
'permanent_blacklist': False,
|
|
'permanent_after_blocks': 3,
|
|
}
|
|
|
|
admin_attempts = defaultdict(list)
|
|
|
|
|
|
def get_client_identifier():
|
|
ip = request.remote_addr
|
|
user_agent = request.headers.get('User-Agent', 'unknown')[:100]
|
|
identifier = hashlib.sha256(f"{ip}:{user_agent}".encode()).hexdigest()
|
|
return identifier, ip
|
|
|
|
|
|
def get_ip_block_info(ip: str) -> dict:
|
|
if redis_client.exists(f"{BLACKLIST_PREFIX}{ip}"):
|
|
return {'blocked': True, 'permanent': True, 'reason': 'Постоянная блокировка', 'until': None}
|
|
|
|
block_key = f"{BLOCKED_PREFIX}{ip}"
|
|
ttl = redis_client.ttl(block_key)
|
|
if ttl > 0:
|
|
return {
|
|
'blocked': True,
|
|
'permanent': False,
|
|
'reason': 'Слишком много попыток',
|
|
'until': time.time() + ttl,
|
|
'remaining_seconds': ttl,
|
|
'remaining_minutes': ttl // 60
|
|
}
|
|
return {'blocked': False}
|
|
|
|
|
|
def get_attempts_count(ip: str) -> int:
|
|
attempts_key = f"{ATTEMPTS_PREFIX}{ip}"
|
|
attempts_data = redis_client.get(attempts_key)
|
|
if not attempts_data:
|
|
return 0
|
|
try:
|
|
attempts = json.loads(attempts_data)
|
|
current_time = time.time()
|
|
valid_attempts = [t for t in attempts if current_time - t < SECURITY_CONFIG['attempt_window']]
|
|
return len(valid_attempts)
|
|
except:
|
|
return 0
|
|
|
|
|
|
def block_ip(ip: str):
|
|
block_count_key = f"{BLOCKED_PREFIX}{ip}:count"
|
|
block_count = int(redis_client.get(block_count_key) or 0)
|
|
|
|
block_duration = min(
|
|
SECURITY_CONFIG['block_duration'] + (block_count * SECURITY_CONFIG['block_duration_increment']),
|
|
SECURITY_CONFIG['max_block_duration']
|
|
)
|
|
|
|
redis_client.setex(f"{BLOCKED_PREFIX}{ip}", block_duration, '1')
|
|
redis_client.setex(block_count_key, 86400 * 30, str(block_count + 1))
|
|
redis_client.delete(f"{ATTEMPTS_PREFIX}{ip}")
|
|
|
|
database.log_security_event(
|
|
ip=ip,
|
|
event_type='temporary_block',
|
|
details=f'Блокировка на {block_duration} секунд (попытка #{block_count + 1})'
|
|
)
|
|
|
|
|
|
def record_failed_attempt(identifier: str):
|
|
_, ip = get_client_identifier()
|
|
attempts_key = f"{ATTEMPTS_PREFIX}{ip}"
|
|
current_time = time.time()
|
|
|
|
attempts_data = redis_client.get(attempts_key)
|
|
attempts = []
|
|
if attempts_data:
|
|
try:
|
|
attempts = json.loads(attempts_data)
|
|
attempts = [t for t in attempts if current_time - t < SECURITY_CONFIG['attempt_window']]
|
|
except:
|
|
attempts = []
|
|
|
|
attempts.append(current_time)
|
|
redis_client.setex(attempts_key, SECURITY_CONFIG['attempt_window'] * 2, json.dumps(attempts))
|
|
|
|
if len(attempts) >= SECURITY_CONFIG['max_attempts']:
|
|
block_ip(ip)
|
|
|
|
|
|
def clear_failed_attempts(identifier: str):
|
|
_, ip = get_client_identifier()
|
|
redis_client.delete(f"{ATTEMPTS_PREFIX}{ip}")
|
|
|
|
|
|
def get_blocked_ips() -> list:
|
|
result = []
|
|
|
|
cursor = 0
|
|
try:
|
|
while True:
|
|
cursor, keys = redis_client.scan(
|
|
cursor,
|
|
match=f"{BLOCKED_PREFIX}*",
|
|
count=100
|
|
)
|
|
|
|
for key in keys:
|
|
if key.endswith(':count'):
|
|
continue
|
|
ip = key.replace(BLOCKED_PREFIX, '')
|
|
ttl = redis_client.ttl(key)
|
|
if ttl > 0:
|
|
count_key = f"{BLOCKED_PREFIX}{ip}:count"
|
|
count = int(redis_client.get(count_key) or 1)
|
|
result.append({
|
|
'ip': ip,
|
|
'permanent': False,
|
|
'remaining_seconds': ttl,
|
|
'remaining_minutes': ttl // 60,
|
|
'block_count': count
|
|
})
|
|
|
|
if cursor == 0:
|
|
break
|
|
except Exception as e:
|
|
print(f"Redis scan error in get_blocked_ips: {e}")
|
|
return result
|
|
|
|
cursor = 0
|
|
try:
|
|
while True:
|
|
cursor, keys = redis_client.scan(
|
|
cursor,
|
|
match=f"{BLACKLIST_PREFIX}*",
|
|
count=100
|
|
)
|
|
|
|
for key in keys:
|
|
ip = key.replace(BLACKLIST_PREFIX, '')
|
|
result.append({'ip': ip, 'permanent': True, 'block_count': '∞'})
|
|
|
|
if cursor == 0:
|
|
break
|
|
except Exception as e:
|
|
print(f"Redis scan error in get_blocked_ips (blacklist): {e}")
|
|
return result
|
|
|
|
return result
|
|
|
|
|
|
def unblock_ip(ip: str):
|
|
redis_client.delete(f"{BLACKLIST_PREFIX}{ip}")
|
|
redis_client.delete(f"{BLOCKED_PREFIX}{ip}")
|
|
redis_client.delete(f"{ATTEMPTS_PREFIX}{ip}")
|
|
redis_client.delete(f"{BLOCKED_PREFIX}{ip}:count")
|
|
return True
|
|
|
|
|
|
def check_admin_rate_limit(ip: str) -> bool:
|
|
current_time = time.time()
|
|
admin_attempts[ip] = [t for t in admin_attempts[ip] if current_time - t < 300]
|
|
if len(admin_attempts[ip]) >= 5:
|
|
return True
|
|
admin_attempts[ip].append(current_time)
|
|
return False
|