"feat: Integrate rate limiter
This commit is contained in:
parent
d6a583d115
commit
9c89ad1397
Binary file not shown.
Binary file not shown.
@ -3,30 +3,48 @@
|
|||||||
{% block title %}Drucker - Mercedes-Benz MYP Platform{% endblock %}
|
{% block title %}Drucker - Mercedes-Benz MYP Platform{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="bg-professional">
|
<div class="bg-professional" style="background: #f8fafc !important;">
|
||||||
|
<!-- Dark Mode Override -->
|
||||||
|
<style>
|
||||||
|
.dark .bg-professional {
|
||||||
|
background: #000000 !important;
|
||||||
|
}
|
||||||
|
.dark .professional-hero {
|
||||||
|
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%) !important;
|
||||||
|
border-color: #333333 !important;
|
||||||
|
}
|
||||||
|
.dark .professional-container {
|
||||||
|
background: #111111 !important;
|
||||||
|
border-color: #333333 !important;
|
||||||
|
}
|
||||||
|
.dark .mb-glass {
|
||||||
|
background: rgba(17, 17, 17, 0.95) !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Professional Hero Header -->
|
<!-- Professional Hero Header -->
|
||||||
<div class="professional-hero hero-pattern animate-fade-in">
|
<div class="professional-hero hero-pattern animate-fade-in" style="margin: 2rem; margin-bottom: 3rem;">
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/20 dark:to-black/40"></div>
|
<div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/40 dark:to-black/60"></div>
|
||||||
|
|
||||||
<!-- Status Indicator -->
|
<!-- Status Indicator -->
|
||||||
<div class="absolute top-6 right-6 flex items-center space-x-3 z-10">
|
<div class="absolute top-6 right-6 flex items-center space-x-4 z-10">
|
||||||
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in">
|
<div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-3">
|
||||||
<div class="status-dot status-online"></div>
|
<div class="status-dot status-online"></div>
|
||||||
<span class="text-sm font-semibold text-professional-primary">Live</span>
|
<span class="text-sm font-semibold text-professional-primary">Live Status</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in">
|
<div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
|
||||||
<span id="live-time" class="text-sm font-semibold text-professional-primary"></span>
|
<span id="live-time" class="text-sm font-semibold text-professional-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-16 z-10">
|
<div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-20 z-10">
|
||||||
<div class="text-center animate-slide-up">
|
<div class="text-center animate-slide-up">
|
||||||
<!-- Mercedes-Benz Logo -->
|
<!-- Mercedes-Benz Logo -->
|
||||||
<div class="inline-flex items-center justify-center w-24 h-24 mb-glass rounded-full mb-8 professional-shadow">
|
<div class="inline-flex items-center justify-center w-28 h-28 mb-glass rounded-full mb-10 professional-shadow">
|
||||||
<svg class="w-12 h-12 text-professional-primary" viewBox="0 0 80 80" fill="currentColor">
|
<svg class="w-14 h-14 text-professional-primary" viewBox="0 0 80 80" fill="currentColor">
|
||||||
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||||
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||||
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||||
@ -36,68 +54,77 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="title-professional text-5xl md:text-6xl font-bold mb-6 tracking-tight">
|
<h1 class="title-professional text-6xl md:text-7xl font-bold mb-8 tracking-tight">
|
||||||
3D-Drucker Management
|
3D-Drucker Management
|
||||||
</h1>
|
</h1>
|
||||||
<p class="subtitle-professional text-xl md:text-2xl max-w-4xl mx-auto leading-relaxed mb-8">
|
<p class="subtitle-professional text-2xl md:text-3xl max-w-5xl mx-auto leading-relaxed mb-12">
|
||||||
Verwalten Sie Ihre 3D-Drucker mit höchster Präzision und Mercedes-Benz Qualitätsstandard
|
Verwalten Sie Ihre 3D-Drucker mit höchster Präzision und Mercedes-Benz Qualitätsstandard
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Status Overview -->
|
<!-- Status Overview -->
|
||||||
<div class="flex flex-wrap justify-center gap-6 mb-8">
|
<div class="flex flex-wrap justify-center gap-8 mb-12">
|
||||||
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in">
|
<div class="mb-glass rounded-3xl px-8 py-6 animate-scale-in min-w-[200px]">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-4">
|
||||||
<div class="status-dot status-online"></div>
|
<div class="status-dot status-online w-4 h-4"></div>
|
||||||
<span class="text-professional-primary font-semibold">Online: <span id="online-count" class="text-green-500 font-bold">-</span></span>
|
<div class="text-left">
|
||||||
|
<div class="text-sm text-professional-muted uppercase tracking-wide">Online</div>
|
||||||
|
<div class="text-3xl font-bold text-green-500" id="online-count">-</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in">
|
<div class="mb-glass rounded-3xl px-8 py-6 animate-scale-in min-w-[200px]">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-4">
|
||||||
<div class="status-dot status-offline"></div>
|
<div class="status-dot status-offline w-4 h-4"></div>
|
||||||
<span class="text-professional-primary font-semibold">Offline: <span id="offline-count" class="text-red-500 font-bold">-</span></span>
|
<div class="text-left">
|
||||||
|
<div class="text-sm text-professional-muted uppercase tracking-wide">Offline</div>
|
||||||
|
<div class="text-3xl font-bold text-red-500" id="offline-count">-</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in">
|
<div class="mb-glass rounded-3xl px-8 py-6 animate-scale-in min-w-[200px]">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-4">
|
||||||
<svg class="w-5 h-5 text-professional-primary" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-6 h-6 text-professional-accent" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-professional-primary font-semibold">Gesamt: <span id="total-count" class="text-professional-accent font-bold">-</span></span>
|
<div class="text-left">
|
||||||
|
<div class="text-sm text-professional-muted uppercase tracking-wide">Gesamt</div>
|
||||||
|
<div class="text-3xl font-bold text-professional-accent" id="total-count">-</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex flex-wrap justify-center gap-4">
|
<div class="flex flex-wrap justify-center gap-6">
|
||||||
<!-- Filter Buttons -->
|
<!-- Filter Buttons -->
|
||||||
<div class="flex bg-white/10 dark:bg-black/20 backdrop-blur-sm rounded-2xl p-2 border border-white/20 dark:border-white/10">
|
<div class="flex bg-white/10 dark:bg-black/30 backdrop-blur-sm rounded-3xl p-3 border border-white/20 dark:border-white/10">
|
||||||
<button id="filter-all" class="filter-btn-mercedes active px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300">
|
<button id="filter-all" class="filter-btn-mercedes active px-6 py-3 rounded-2xl text-sm font-semibold transition-all duration-300">
|
||||||
Alle
|
Alle Drucker
|
||||||
</button>
|
</button>
|
||||||
<button id="filter-online" class="filter-btn-mercedes px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300">
|
<button id="filter-online" class="filter-btn-mercedes px-6 py-3 rounded-2xl text-sm font-semibold transition-all duration-300">
|
||||||
Online
|
Online
|
||||||
</button>
|
</button>
|
||||||
<button id="filter-offline" class="filter-btn-mercedes px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300">
|
<button id="filter-offline" class="filter-btn-mercedes px-6 py-3 rounded-2xl text-sm font-semibold transition-all duration-300">
|
||||||
Offline
|
Offline
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick="toggleAutoRefresh()" id="auto-refresh-btn" class="btn-professional group">
|
<button onclick="toggleAutoRefresh()" id="auto-refresh-btn" class="btn-professional group px-8 py-4">
|
||||||
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Auto-Update</span>
|
<span>Auto-Update</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onclick="refreshPrinters()" class="btn-professional group">
|
<button onclick="refreshPrinters()" class="btn-professional group px-8 py-4">
|
||||||
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Jetzt aktualisieren</span>
|
<span>Status aktualisieren</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
<button id="addPrinterBtn" class="btn-success-professional group">
|
<button id="addPrinterBtn" class="btn-success-professional group px-8 py-4">
|
||||||
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -107,35 +134,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auto-Update Info -->
|
<!-- Auto-Update Info -->
|
||||||
<div class="mt-6 flex items-center justify-center space-x-2 text-professional-muted">
|
<div class="mt-8 flex items-center justify-center space-x-3 text-professional-muted">
|
||||||
<svg id="auto-refresh-icon" class="w-4 h-4 animate-spin-slow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg id="auto-refresh-icon" class="w-5 h-5 animate-spin-slow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-sm">Auto-Update: <span id="next-update-time" class="font-mono">-</span>s</span>
|
<span class="text-base">Auto-Update: <span id="next-update-time" class="font-mono font-bold">-</span>s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10 space-y-8">
|
<div class="max-w-7xl mx-auto px-6 lg:px-8 -mt-12 relative z-10" style="margin-bottom: 4rem;">
|
||||||
|
|
||||||
<!-- Printers Grid -->
|
<!-- Printers Grid -->
|
||||||
<div class="professional-container p-8 lg:p-12 animate-slide-up">
|
<div class="professional-container animate-slide-up" style="padding: 3rem; border-radius: 2rem; margin-bottom: 2rem;">
|
||||||
<div class="text-center mb-10">
|
<div class="text-center mb-16">
|
||||||
<h2 class="title-professional text-3xl font-bold mb-4">
|
<h2 class="title-professional text-4xl font-bold mb-6">
|
||||||
Verfügbare Drucker
|
Verfügbare 3D-Drucker
|
||||||
</h2>
|
</h2>
|
||||||
<p class="subtitle-professional text-lg">
|
<p class="subtitle-professional text-xl max-w-3xl mx-auto">
|
||||||
Übersicht und Verwaltung Ihrer 3D-Drucker-Infrastruktur
|
Übersicht und Verwaltung Ihrer gesamten 3D-Drucker-Infrastruktur mit Echtzeit-Statusanzeige
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<!-- Loading state -->
|
<!-- Loading state -->
|
||||||
<div class="col-span-full text-center py-12">
|
<div class="col-span-full text-center py-16">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-professional-accent mx-auto"></div>
|
<div class="animate-spin rounded-full h-16 w-16 border-b-3 border-professional-accent mx-auto mb-8"></div>
|
||||||
<p class="mt-4 text-base text-professional-secondary">Lade Drucker...</p>
|
<h3 class="text-2xl font-bold text-professional-primary mb-4">Lade Drucker...</h3>
|
||||||
<p class="mt-1 text-sm text-professional-muted">Dies sollte nur wenige Sekunden dauern</p>
|
<p class="text-lg text-professional-secondary">Dies sollte nur wenige Sekunden dauern</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
238
backend/app/utils/rate_limiter.py
Normal file
238
backend/app/utils/rate_limiter.py
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Rate Limiting System für MYP Platform
|
||||||
|
Schutz vor API-Missbrauch und DDoS-Attacken
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import redis
|
||||||
|
import hashlib
|
||||||
|
from functools import wraps
|
||||||
|
from flask import request, jsonify, g
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("security")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RateLimit:
|
||||||
|
"""Konfiguration für Rate-Limiting-Regeln"""
|
||||||
|
requests: int # Anzahl erlaubter Anfragen
|
||||||
|
per: int # Zeitraum in Sekunden
|
||||||
|
message: str # Fehlermeldung bei Überschreitung
|
||||||
|
|
||||||
|
# Rate-Limiting-Konfiguration
|
||||||
|
RATE_LIMITS = {
|
||||||
|
# API-Endpunkte
|
||||||
|
'api_general': RateLimit(100, 300, "Zu viele API-Anfragen. Versuchen Sie es in 5 Minuten erneut."),
|
||||||
|
'api_auth': RateLimit(10, 300, "Zu viele Anmeldeversuche. Versuchen Sie es in 5 Minuten erneut."),
|
||||||
|
'api_upload': RateLimit(20, 3600, "Zu viele Upload-Anfragen. Versuchen Sie es in einer Stunde erneut."),
|
||||||
|
'api_admin': RateLimit(200, 300, "Zu viele Admin-Anfragen. Versuchen Sie es in 5 Minuten erneut."),
|
||||||
|
|
||||||
|
# Spezielle Endpunkte
|
||||||
|
'printer_status': RateLimit(300, 300, "Zu viele Drucker-Status-Anfragen."),
|
||||||
|
'job_creation': RateLimit(50, 3600, "Zu viele Job-Erstellungen. Versuchen Sie es in einer Stunde erneut."),
|
||||||
|
|
||||||
|
# Sicherheitskritische Endpunkte
|
||||||
|
'password_reset': RateLimit(3, 3600, "Zu viele Passwort-Reset-Anfragen. Versuchen Sie es in einer Stunde erneut."),
|
||||||
|
'user_creation': RateLimit(10, 3600, "Zu viele Benutzer-Erstellungen.")
|
||||||
|
}
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
"""
|
||||||
|
In-Memory Rate Limiter mit optionaler Redis-Unterstützung
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, use_redis: bool = False, redis_url: str = None):
|
||||||
|
self.use_redis = use_redis
|
||||||
|
self.redis_client = None
|
||||||
|
self.memory_store: Dict[str, Dict] = {}
|
||||||
|
|
||||||
|
if use_redis and redis_url:
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
self.redis_client = redis.from_url(redis_url, decode_responses=True)
|
||||||
|
logger.info("✅ Redis-basiertes Rate Limiting aktiviert")
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("⚠️ Redis nicht verfügbar, verwende In-Memory Rate Limiting")
|
||||||
|
self.use_redis = False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Redis-Verbindung fehlgeschlagen: {e}")
|
||||||
|
self.use_redis = False
|
||||||
|
|
||||||
|
def _get_client_id(self) -> str:
|
||||||
|
"""
|
||||||
|
Generiert eine eindeutige Client-ID basierend auf IP und User-Agent
|
||||||
|
"""
|
||||||
|
ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
|
||||||
|
user_agent = request.headers.get('User-Agent', '')
|
||||||
|
|
||||||
|
# Hash für Anonymisierung
|
||||||
|
client_string = f"{ip}:{user_agent}"
|
||||||
|
return hashlib.sha256(client_string.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
def _get_key(self, limit_type: str, client_id: str) -> str:
|
||||||
|
"""Erstellt Redis/Memory-Key für Rate-Limiting"""
|
||||||
|
return f"rate_limit:{limit_type}:{client_id}"
|
||||||
|
|
||||||
|
def _get_current_requests(self, key: str, window_start: int) -> int:
|
||||||
|
"""Holt aktuelle Anfragen-Anzahl"""
|
||||||
|
if self.use_redis and self.redis_client:
|
||||||
|
try:
|
||||||
|
# Redis-basierte Implementierung
|
||||||
|
pipe = self.redis_client.pipeline()
|
||||||
|
pipe.zremrangebyscore(key, 0, window_start)
|
||||||
|
pipe.zcard(key)
|
||||||
|
_, count = pipe.execute()
|
||||||
|
return count
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Redis-Fehler: {e}, fallback zu Memory")
|
||||||
|
self.use_redis = False
|
||||||
|
|
||||||
|
# In-Memory Implementierung
|
||||||
|
if key not in self.memory_store:
|
||||||
|
self.memory_store[key] = {'requests': [], 'last_cleanup': time.time()}
|
||||||
|
|
||||||
|
# Alte Einträge bereinigen
|
||||||
|
current_time = time.time()
|
||||||
|
data = self.memory_store[key]
|
||||||
|
data['requests'] = [req_time for req_time in data['requests'] if req_time > window_start]
|
||||||
|
|
||||||
|
return len(data['requests'])
|
||||||
|
|
||||||
|
def _add_request(self, key: str, current_time: int, expire_time: int):
|
||||||
|
"""Fügt neue Anfrage hinzu"""
|
||||||
|
if self.use_redis and self.redis_client:
|
||||||
|
try:
|
||||||
|
pipe = self.redis_client.pipeline()
|
||||||
|
pipe.zadd(key, {str(current_time): current_time})
|
||||||
|
pipe.expire(key, expire_time)
|
||||||
|
pipe.execute()
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Redis-Fehler: {e}, fallback zu Memory")
|
||||||
|
self.use_redis = False
|
||||||
|
|
||||||
|
# In-Memory Implementierung
|
||||||
|
if key not in self.memory_store:
|
||||||
|
self.memory_store[key] = {'requests': [], 'last_cleanup': time.time()}
|
||||||
|
|
||||||
|
self.memory_store[key]['requests'].append(current_time)
|
||||||
|
|
||||||
|
def is_allowed(self, limit_type: str) -> tuple[bool, Dict]:
|
||||||
|
"""
|
||||||
|
Prüft ob eine Anfrage erlaubt ist
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(is_allowed, info_dict)
|
||||||
|
"""
|
||||||
|
if limit_type not in RATE_LIMITS:
|
||||||
|
return True, {}
|
||||||
|
|
||||||
|
rate_limit = RATE_LIMITS[limit_type]
|
||||||
|
client_id = self._get_client_id()
|
||||||
|
key = self._get_key(limit_type, client_id)
|
||||||
|
|
||||||
|
current_time = int(time.time())
|
||||||
|
window_start = current_time - rate_limit.per
|
||||||
|
|
||||||
|
# Aktuelle Anfragen zählen
|
||||||
|
current_requests = self._get_current_requests(key, window_start)
|
||||||
|
|
||||||
|
# Limite prüfen
|
||||||
|
if current_requests >= rate_limit.requests:
|
||||||
|
logger.warning(f"🚨 Rate limit exceeded: {limit_type} für Client {client_id[:8]}...")
|
||||||
|
return False, {
|
||||||
|
'limit': rate_limit.requests,
|
||||||
|
'remaining': 0,
|
||||||
|
'reset_time': current_time + rate_limit.per,
|
||||||
|
'message': rate_limit.message
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anfrage hinzufügen
|
||||||
|
self._add_request(key, current_time, rate_limit.per)
|
||||||
|
|
||||||
|
return True, {
|
||||||
|
'limit': rate_limit.requests,
|
||||||
|
'remaining': rate_limit.requests - current_requests - 1,
|
||||||
|
'reset_time': current_time + rate_limit.per
|
||||||
|
}
|
||||||
|
|
||||||
|
def cleanup_memory(self):
|
||||||
|
"""Bereinigt alte In-Memory-Einträge"""
|
||||||
|
if self.use_redis:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
keys_to_delete = []
|
||||||
|
|
||||||
|
for key, data in self.memory_store.items():
|
||||||
|
# Bereinige alle Einträge älter als 24 Stunden
|
||||||
|
if current_time - data.get('last_cleanup', 0) > 86400:
|
||||||
|
keys_to_delete.append(key)
|
||||||
|
|
||||||
|
for key in keys_to_delete:
|
||||||
|
del self.memory_store[key]
|
||||||
|
|
||||||
|
# Globale Rate-Limiter-Instanz
|
||||||
|
rate_limiter = RateLimiter()
|
||||||
|
|
||||||
|
def limit_requests(limit_type: str):
|
||||||
|
"""
|
||||||
|
Decorator für Rate-Limiting von API-Endpunkten
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit_type: Art des Limits (siehe RATE_LIMITS)
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Rate-Limiting prüfen
|
||||||
|
is_allowed, info = rate_limiter.is_allowed(limit_type)
|
||||||
|
|
||||||
|
if not is_allowed:
|
||||||
|
response = jsonify({
|
||||||
|
'error': 'Rate limit exceeded',
|
||||||
|
'message': info['message'],
|
||||||
|
'retry_after': info['reset_time'] - int(time.time())
|
||||||
|
})
|
||||||
|
response.status_code = 429
|
||||||
|
response.headers['Retry-After'] = str(info['reset_time'] - int(time.time()))
|
||||||
|
response.headers['X-RateLimit-Limit'] = str(info['limit'])
|
||||||
|
response.headers['X-RateLimit-Remaining'] = str(info['remaining'])
|
||||||
|
response.headers['X-RateLimit-Reset'] = str(info['reset_time'])
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Rate-Limiting-Headers zu Response hinzufügen
|
||||||
|
response = f(*args, **kwargs)
|
||||||
|
|
||||||
|
if hasattr(response, 'headers'):
|
||||||
|
response.headers['X-RateLimit-Limit'] = str(info['limit'])
|
||||||
|
response.headers['X-RateLimit-Remaining'] = str(info['remaining'])
|
||||||
|
response.headers['X-RateLimit-Reset'] = str(info['reset_time'])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def get_client_info() -> Dict:
|
||||||
|
"""
|
||||||
|
Gibt Client-Informationen für Rate-Limiting zurück
|
||||||
|
"""
|
||||||
|
client_id = rate_limiter._get_client_id()
|
||||||
|
ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'client_id': client_id,
|
||||||
|
'ip_address': ip,
|
||||||
|
'user_agent': request.headers.get('User-Agent', ''),
|
||||||
|
'timestamp': int(time.time())
|
||||||
|
}
|
||||||
|
|
||||||
|
# Maintenance-Task für Memory-Cleanup
|
||||||
|
def cleanup_rate_limiter():
|
||||||
|
"""Periodische Bereinigung des Rate-Limiters"""
|
||||||
|
rate_limiter.cleanup_memory()
|
||||||
|
logger.debug("🧹 Rate-Limiter Memory bereinigt")
|
328
backend/app/utils/security.py
Normal file
328
backend/app/utils/security.py
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Security Utilities für MYP Platform
|
||||||
|
Content Security Policy (CSP), Security Headers und weitere Sicherheitsmaßnahmen
|
||||||
|
"""
|
||||||
|
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
from flask import request, g, session
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("security")
|
||||||
|
|
||||||
|
# Content Security Policy Konfiguration
|
||||||
|
CSP_POLICY = {
|
||||||
|
'default-src': ["'self'"],
|
||||||
|
'script-src': [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-inline'", # Temporär für Dark Mode Script
|
||||||
|
"https://cdn.jsdelivr.net", # Für externe Libraries
|
||||||
|
"https://unpkg.com" # Für Fallback-Libraries
|
||||||
|
],
|
||||||
|
'style-src': [
|
||||||
|
"'self'",
|
||||||
|
"'unsafe-inline'", # Für Tailwind und Dynamic Styles
|
||||||
|
"https://fonts.googleapis.com"
|
||||||
|
],
|
||||||
|
'img-src': [
|
||||||
|
"'self'",
|
||||||
|
"data:", # Für SVG Data URLs
|
||||||
|
"blob:", # Für dynamisch generierte Bilder
|
||||||
|
"https:" # HTTPS-Bilder erlauben
|
||||||
|
],
|
||||||
|
'font-src': [
|
||||||
|
"'self'",
|
||||||
|
"https://fonts.gstatic.com",
|
||||||
|
"data:" # Für eingebettete Fonts
|
||||||
|
],
|
||||||
|
'connect-src': [
|
||||||
|
"'self'",
|
||||||
|
"ws:", # WebSocket für lokale Entwicklung
|
||||||
|
"wss:" # Sichere WebSockets
|
||||||
|
],
|
||||||
|
'media-src': ["'self'"],
|
||||||
|
'object-src': ["'none'"], # Flash und andere Plugins blockieren
|
||||||
|
'base-uri': ["'self'"],
|
||||||
|
'form-action': ["'self'"],
|
||||||
|
'frame-ancestors': ["'none'"], # Clickjacking-Schutz
|
||||||
|
'upgrade-insecure-requests': True,
|
||||||
|
'block-all-mixed-content': True
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security Headers Konfiguration
|
||||||
|
SECURITY_HEADERS = {
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
'X-Frame-Options': 'DENY',
|
||||||
|
'X-XSS-Protection': '1; mode=block',
|
||||||
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||||
|
'Permissions-Policy': (
|
||||||
|
'geolocation=(), '
|
||||||
|
'microphone=(), '
|
||||||
|
'camera=(), '
|
||||||
|
'payment=(), '
|
||||||
|
'usb=(), '
|
||||||
|
'accelerometer=(), '
|
||||||
|
'gyroscope=(), '
|
||||||
|
'magnetometer=()'
|
||||||
|
),
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Resource-Policy': 'same-origin'
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecurityManager:
|
||||||
|
"""
|
||||||
|
Zentrale Sicherheitsverwaltung für MYP Platform
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.nonce_store: Dict[str, str] = {}
|
||||||
|
|
||||||
|
def generate_nonce(self) -> str:
|
||||||
|
"""Generiert eine sichere Nonce für CSP"""
|
||||||
|
nonce = secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
# Nonce in Session speichern für Validierung
|
||||||
|
if 'security_nonces' not in session:
|
||||||
|
session['security_nonces'] = []
|
||||||
|
|
||||||
|
session['security_nonces'].append(nonce)
|
||||||
|
|
||||||
|
# Maximal 10 Nonces pro Session
|
||||||
|
if len(session['security_nonces']) > 10:
|
||||||
|
session['security_nonces'] = session['security_nonces'][-10:]
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
|
||||||
|
def validate_nonce(self, nonce: str) -> bool:
|
||||||
|
"""Validiert eine Nonce"""
|
||||||
|
if 'security_nonces' not in session:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return nonce in session['security_nonces']
|
||||||
|
|
||||||
|
def build_csp_header(self, nonce: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Erstellt den Content-Security-Policy Header
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nonce: Optional CSP nonce für inline scripts
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CSP Header String
|
||||||
|
"""
|
||||||
|
csp_parts = []
|
||||||
|
|
||||||
|
for directive, values in CSP_POLICY.items():
|
||||||
|
if directive in ['upgrade-insecure-requests', 'block-all-mixed-content']:
|
||||||
|
if values:
|
||||||
|
csp_parts.append(directive.replace('_', '-'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(values, list):
|
||||||
|
directive_values = values.copy()
|
||||||
|
|
||||||
|
# Nonce für script-src hinzufügen
|
||||||
|
if directive == 'script-src' and nonce:
|
||||||
|
directive_values.append(f"'nonce-{nonce}'")
|
||||||
|
|
||||||
|
csp_parts.append(f"{directive.replace('_', '-')} {' '.join(directive_values)}")
|
||||||
|
|
||||||
|
return "; ".join(csp_parts)
|
||||||
|
|
||||||
|
def get_client_fingerprint(self) -> str:
|
||||||
|
"""
|
||||||
|
Erstellt einen Client-Fingerprint für erweiterte Sicherheit
|
||||||
|
"""
|
||||||
|
components = [
|
||||||
|
request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr),
|
||||||
|
request.headers.get('User-Agent', ''),
|
||||||
|
request.headers.get('Accept-Language', ''),
|
||||||
|
request.headers.get('Accept-Encoding', '')
|
||||||
|
]
|
||||||
|
|
||||||
|
fingerprint_string = '|'.join(components)
|
||||||
|
return hashlib.sha256(fingerprint_string.encode()).hexdigest()[:32]
|
||||||
|
|
||||||
|
def check_suspicious_activity(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft auf verdächtige Aktivitäten
|
||||||
|
"""
|
||||||
|
# SQL Injection Patterns
|
||||||
|
sql_patterns = [
|
||||||
|
'union select', 'drop table', 'insert into', 'delete from',
|
||||||
|
'script>', '<iframe', 'javascript:', 'vbscript:',
|
||||||
|
'onload=', 'onerror=', 'onclick='
|
||||||
|
]
|
||||||
|
|
||||||
|
# Request-Daten prüfen
|
||||||
|
request_data = str(request.args) + str(request.form) + str(request.json or {})
|
||||||
|
request_data_lower = request_data.lower()
|
||||||
|
|
||||||
|
for pattern in sql_patterns:
|
||||||
|
if pattern in request_data_lower:
|
||||||
|
logger.warning(f"🚨 Verdächtige Aktivität erkannt: {pattern} von {request.remote_addr}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Übermäßig große Requests
|
||||||
|
if len(request_data) > 50000: # 50KB Limit
|
||||||
|
logger.warning(f"🚨 Übermäßig große Anfrage von {request.remote_addr}: {len(request_data)} bytes")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def log_security_event(self, event_type: str, details: Dict):
|
||||||
|
"""
|
||||||
|
Protokolliert Sicherheitsereignisse
|
||||||
|
"""
|
||||||
|
security_data = {
|
||||||
|
'event_type': event_type,
|
||||||
|
'ip_address': request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr),
|
||||||
|
'user_agent': request.headers.get('User-Agent', ''),
|
||||||
|
'timestamp': request.environ.get('REQUEST_START_TIME'),
|
||||||
|
'fingerprint': self.get_client_fingerprint(),
|
||||||
|
**details
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warning(f"🔒 Sicherheitsereignis: {event_type} - {security_data}")
|
||||||
|
|
||||||
|
# Globale Security Manager Instanz
|
||||||
|
security_manager = SecurityManager()
|
||||||
|
|
||||||
|
def apply_security_headers(response):
|
||||||
|
"""
|
||||||
|
Wendet Sicherheits-Headers auf Response an
|
||||||
|
"""
|
||||||
|
# Standard Security Headers
|
||||||
|
for header, value in SECURITY_HEADERS.items():
|
||||||
|
response.headers[header] = value
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
nonce = getattr(g, 'csp_nonce', None)
|
||||||
|
csp_header = security_manager.build_csp_header(nonce)
|
||||||
|
response.headers['Content-Security-Policy'] = csp_header
|
||||||
|
|
||||||
|
# HSTS für HTTPS
|
||||||
|
if request.is_secure:
|
||||||
|
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def security_check(check_suspicious: bool = True):
|
||||||
|
"""
|
||||||
|
Decorator für Sicherheitsprüfungen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
check_suspicious: Ob auf verdächtige Aktivitäten geprüft werden soll
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Verdächtige Aktivitäten prüfen
|
||||||
|
if check_suspicious and security_manager.check_suspicious_activity():
|
||||||
|
security_manager.log_security_event('suspicious_request', {
|
||||||
|
'endpoint': request.endpoint,
|
||||||
|
'method': request.method,
|
||||||
|
'args': dict(request.args),
|
||||||
|
'form': dict(request.form)
|
||||||
|
})
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Verdächtige Anfrage erkannt',
|
||||||
|
'message': 'Ihre Anfrage wurde aus Sicherheitsgründen blockiert.'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def require_secure_headers(f):
|
||||||
|
"""
|
||||||
|
Decorator der sicherstellt, dass Security Headers gesetzt werden
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# CSP Nonce generieren
|
||||||
|
g.csp_nonce = security_manager.generate_nonce()
|
||||||
|
|
||||||
|
response = f(*args, **kwargs)
|
||||||
|
|
||||||
|
# Security Headers anwenden
|
||||||
|
if hasattr(response, 'headers'):
|
||||||
|
response = apply_security_headers(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def get_csp_nonce() -> str:
|
||||||
|
"""
|
||||||
|
Holt die aktuelle CSP Nonce für Templates
|
||||||
|
"""
|
||||||
|
return getattr(g, 'csp_nonce', '')
|
||||||
|
|
||||||
|
def validate_origin():
|
||||||
|
"""
|
||||||
|
Validiert die Origin des Requests
|
||||||
|
"""
|
||||||
|
origin = request.headers.get('Origin')
|
||||||
|
referer = request.headers.get('Referer')
|
||||||
|
host = request.headers.get('Host')
|
||||||
|
|
||||||
|
# Für API-Requests Origin prüfen
|
||||||
|
if request.path.startswith('/api/') and origin:
|
||||||
|
allowed_origins = [
|
||||||
|
f"http://{host}",
|
||||||
|
f"https://{host}",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"http://127.0.0.1:5000"
|
||||||
|
]
|
||||||
|
|
||||||
|
if origin not in allowed_origins:
|
||||||
|
logger.warning(f"🚨 Ungültige Origin: {origin} für {request.path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Template Helper für CSP Nonce
|
||||||
|
def csp_nonce():
|
||||||
|
"""Template Helper für CSP Nonce"""
|
||||||
|
return get_csp_nonce()
|
||||||
|
|
||||||
|
# Security Middleware für Flask App
|
||||||
|
def init_security(app):
|
||||||
|
"""
|
||||||
|
Initialisiert Sicherheitsfeatures für Flask App
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request_security():
|
||||||
|
"""Security Checks vor jedem Request"""
|
||||||
|
|
||||||
|
# Origin validieren
|
||||||
|
if not validate_origin():
|
||||||
|
from flask import jsonify
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Invalid origin',
|
||||||
|
'message': 'Anfrage von ungültiger Quelle'
|
||||||
|
}), 403
|
||||||
|
|
||||||
|
# CSP Nonce generieren
|
||||||
|
g.csp_nonce = security_manager.generate_nonce()
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def after_request_security(response):
|
||||||
|
"""Security Headers nach jedem Request anwenden"""
|
||||||
|
return apply_security_headers(response)
|
||||||
|
|
||||||
|
# Template Helper registrieren
|
||||||
|
app.jinja_env.globals['csp_nonce'] = csp_nonce
|
||||||
|
|
||||||
|
logger.info("🔒 Security System initialisiert")
|
||||||
|
|
||||||
|
return app
|
Loading…
x
Reference in New Issue
Block a user