#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ MYP Druckerverwaltung - OPTIMIERTE PRODUKTIONS-VERSION ===================================================== Standalone Flask App für Raspberry Pi Produktionsbetrieb: - Nur HTTPS Port 443 (kein HTTP Port 5000) - Browser-kompatible SSL-Zertifikate - Optimierte Performance für Kiosk-Modus - Minimale Firewall-Exposition - Keine Proxy-Dependencies Version: 5.0.0 Production """ import os import sys import ssl import logging import platform from datetime import datetime, timedelta # Füge App-Verzeichnis zum Python-Pfad hinzu sys.path.insert(0, '/opt/myp') sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # Import der Haupt-App from app import app, app_logger # Flask-Imports für Request-Handling from flask import request, redirect # SSL und Sicherheits-Imports from utils.ssl_config import ensure_ssl_certificates, get_ssl_context # =========================== PRODUKTIONS-KONFIGURATION =========================== class ProductionConfig: """Optimierte Produktions-Konfiguration für Raspberry Pi""" # HTTPS-Only Konfiguration FORCE_HTTPS = True SSL_REQUIRED = True HTTPS_PORT = 443 HTTP_DISABLED = True # Performance-Optimierungen DEBUG = False TESTING = False OPTIMIZED_MODE = True USE_MINIFIED_ASSETS = True DISABLE_ANIMATIONS = True # Sicherheits-Einstellungen SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Strict' WTF_CSRF_ENABLED = True # SSL-Konfiguration SSL_CERT_PATH = '/opt/myp/ssl/cert.pem' SSL_KEY_PATH = '/opt/myp/ssl/key.pem' # Firewall-freundliche Konfiguration SINGLE_PORT_MODE = True NO_ADDITIONAL_PORTS = True # Wende Produktions-Konfiguration an app.config.from_object(ProductionConfig) # =========================== SSL-SETUP =========================== def setup_production_ssl(): """Stelle sicher, dass browser-kompatible SSL-Zertifikate vorhanden sind""" # Plattform-spezifische SSL-Pfade if platform.system() == 'Windows': ssl_dir = os.path.join(os.path.dirname(__file__), 'ssl') else: ssl_dir = '/opt/myp/ssl' cert_file = f'{ssl_dir}/cert.pem' key_file = f'{ssl_dir}/key.pem' app_logger.info("🔐 Prüfe SSL-Zertifikate für Produktionsbetrieb...") # Erstelle SSL-Verzeichnis os.makedirs(ssl_dir, exist_ok=True) # Prüfe ob Zertifikate existieren und gültig sind cert_valid = False if os.path.exists(cert_file) and os.path.exists(key_file): try: # Prüfe Zertifikat-Gültigkeit import subprocess result = subprocess.run([ 'openssl', 'x509', '-in', cert_file, '-noout', '-checkend', '86400' ], capture_output=True, text=True) if result.returncode == 0: # Prüfe Browser-Kompatibilität cert_info = subprocess.run([ 'openssl', 'x509', '-in', cert_file, '-noout', '-text' ], capture_output=True, text=True) if ('Digital Signature' in cert_info.stdout and 'Key Encipherment' in cert_info.stdout and 'TLS Web Server Authentication' in cert_info.stdout and 'Subject Alternative Name' in cert_info.stdout): cert_valid = True app_logger.info("✅ Browser-kompatible SSL-Zertifikate gefunden") else: app_logger.warning("⚠️ SSL-Zertifikate nicht browser-kompatibel") else: app_logger.warning("⚠️ SSL-Zertifikate abgelaufen") except Exception as e: app_logger.warning(f"⚠️ SSL-Zertifikat-Prüfung fehlgeschlagen: {e}") # Erstelle neue browser-kompatible Zertifikate falls nötig if not cert_valid: app_logger.info("🔧 Erstelle neue browser-kompatible SSL-Zertifikate...") try: # Führe SSL-Fix-Skript aus falls vorhanden ssl_fix_script = '/opt/myp/fix_ssl_raspberry.sh' if os.path.exists(ssl_fix_script): import subprocess result = subprocess.run(['sudo', ssl_fix_script], capture_output=True, text=True, timeout=60) if result.returncode == 0: app_logger.info("✅ SSL-Fix-Skript erfolgreich ausgeführt") else: app_logger.error(f"❌ SSL-Fix-Skript Fehler: {result.stderr}") raise Exception("SSL-Fix-Skript fehlgeschlagen") else: # Fallback: Manuelle SSL-Erstellung create_production_ssl_certificates(ssl_dir) except Exception as e: app_logger.error(f"❌ SSL-Zertifikat-Erstellung fehlgeschlagen: {e}") raise return cert_file, key_file def create_production_ssl_certificates(ssl_dir): """Erstelle browser-kompatible SSL-Zertifikate manuell""" import subprocess import tempfile app_logger.info("🔧 Erstelle browser-kompatible SSL-Zertifikate...") # OpenSSL-Konfiguration für Browser-Kompatibilität openssl_config = f"""[req] distinguished_name = req_distinguished_name req_extensions = v3_req prompt = no [req_distinguished_name] C = DE ST = Baden-Wuerttemberg L = Stuttgart O = Mercedes-Benz AG OU = MYP Druckerverwaltung CN = m040tbaraspi001 [v3_req] # KRITISCH für Browser-Kompatibilität basicConstraints = critical, CA:FALSE keyUsage = critical, digitalSignature, keyEncipherment, keyAgreement extendedKeyUsage = critical, serverAuth, clientAuth subjectAltName = critical, @alt_names nsCertType = server nsComment = "MYP Production SSL - Browser Compatible" [alt_names] # Lokale Entwicklung DNS.1 = localhost DNS.2 = *.localhost IP.1 = 127.0.0.1 IP.2 = ::1 # Raspberry Pi Hostname DNS.3 = m040tbaraspi001 DNS.4 = m040tbaraspi001.local DNS.5 = raspberrypi DNS.6 = raspberrypi.local # Intranet-Domain DNS.7 = m040tbaraspi001.de040.corpintra.net DNS.8 = *.de040.corpintra.net """ # Schreibe Konfiguration in temporäre Datei with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as f: f.write(openssl_config) config_file = f.name try: # Generiere Private Key subprocess.run([ 'openssl', 'genrsa', '-out', f'{ssl_dir}/key.pem', '2048' ], check=True, capture_output=True) # Generiere browser-kompatibles Zertifikat subprocess.run([ 'openssl', 'req', '-new', '-x509', '-key', f'{ssl_dir}/key.pem', '-out', f'{ssl_dir}/cert.pem', '-days', '365', '-config', config_file, '-extensions', 'v3_req', '-sha256' ], check=True, capture_output=True) # Setze korrekte Berechtigungen os.chmod(f'{ssl_dir}/cert.pem', 0o644) os.chmod(f'{ssl_dir}/key.pem', 0o600) app_logger.info("✅ Browser-kompatible SSL-Zertifikate erstellt") finally: # Räume temporäre Datei auf try: os.unlink(config_file) except: pass # =========================== PRODUKTIONS-SSL-KONTEXT =========================== def get_production_ssl_context(): """Erstelle optimierten SSL-Kontext für Produktionsbetrieb""" cert_file, key_file = setup_production_ssl() # Erstelle SSL-Kontext mit optimalen Einstellungen context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # Lade Zertifikat und Key context.load_cert_chain(cert_file, key_file) # Optimale SSL-Einstellungen für Browser-Kompatibilität context.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS') context.options |= ssl.OP_NO_SSLv2 context.options |= ssl.OP_NO_SSLv3 context.options |= ssl.OP_NO_TLSv1 context.options |= ssl.OP_NO_TLSv1_1 context.options |= ssl.OP_SINGLE_DH_USE context.options |= ssl.OP_SINGLE_ECDH_USE # Deaktiviere Kompression (CRIME-Angriff-Schutz) context.options |= ssl.OP_NO_COMPRESSION app_logger.info("✅ Produktions-SSL-Kontext konfiguriert") return context # =========================== HTTPS-REDIRECT MIDDLEWARE =========================== @app.before_request def force_https(): """Erzwinge HTTPS für alle Anfragen""" if not request.is_secure and app.config.get('FORCE_HTTPS', False): # Redirect zu HTTPS url = request.url.replace('http://', 'https://', 1) # Ändere Port zu 443 falls anders if ':5000' in url: url = url.replace(':5000', ':443') elif ':80' in url: url = url.replace(':80', ':443') return redirect(url, code=301) # =========================== SICHERHEITS-HEADERS =========================== @app.after_request def add_security_headers(response): """Füge Sicherheits-Headers für Produktionsbetrieb hinzu""" # HTTPS-Sicherheits-Headers response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'SAMEORIGIN' response.headers['X-XSS-Protection'] = '1; mode=block' response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' # Content Security Policy für Kiosk-Modus csp = ( "default-src 'self'; " "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " "style-src 'self' 'unsafe-inline'; " "img-src 'self' data: blob:; " "font-src 'self'; " "connect-src 'self'; " "frame-ancestors 'self'" ) response.headers['Content-Security-Policy'] = csp # Cache-Control für statische Assets if request.endpoint and 'static' in request.endpoint: response.headers['Cache-Control'] = 'public, max-age=31536000' return response # =========================== PRODUKTIONS-LOGGING =========================== def setup_production_logging(): """Konfiguriere optimiertes Logging für Produktionsbetrieb""" # Reduziere Log-Level für Performance logging.getLogger('werkzeug').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) # Produktions-Log-Format formatter = logging.Formatter( '%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # Stelle sicher, dass App-Logger korrekt konfiguriert ist app_logger.setLevel(logging.INFO) # Entferne Debug-Handler falls vorhanden for handler in app_logger.handlers[:]: if handler.level == logging.DEBUG: app_logger.removeHandler(handler) app_logger.info("✅ Produktions-Logging konfiguriert") # =========================== HAUPTFUNKTION =========================== def main(): """Hauptfunktion für Produktions-Server""" try: app_logger.info("🚀 MYP Produktions-Server startet...") app_logger.info(f"📅 Start-Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") app_logger.info(f"🖥️ Hostname: {platform.node()}") app_logger.info(f"🐍 Python: {sys.version}") # Produktions-Logging einrichten setup_production_logging() # Prüfe Root-Berechtigung für Port 443 (nur Unix/Linux) if hasattr(os, 'geteuid') and os.geteuid() != 0: app_logger.error("❌ Root-Berechtigung erforderlich für Port 443") app_logger.error("💡 Führe aus mit: sudo python3 app_production.py") sys.exit(1) elif platform.system() == 'Windows': app_logger.info("🪟 Windows-Modus: Root-Check übersprungen") # SSL-Kontext erstellen ssl_context = get_production_ssl_context() # Datenbank initialisieren (aus Haupt-App) from app import init_database, create_initial_admin init_database() create_initial_admin() # Queue Manager und Scheduler starten from app import start_queue_manager, get_job_scheduler start_queue_manager() scheduler = get_job_scheduler() if scheduler: scheduler.start() app_logger.info("✅ Job-Scheduler gestartet") # Server-Konfiguration host = '0.0.0.0' # Alle Interfaces port = 443 # Nur HTTPS Port 443 app_logger.info("🔐 HTTPS-Only Produktions-Modus") app_logger.info(f"🌐 Server läuft auf: https://{host}:{port}") app_logger.info(f"🏠 Lokaler Zugriff: https://localhost") app_logger.info(f"🌍 Intranet-Zugriff: https://m040tbaraspi001.de040.corpintra.net") app_logger.info("🔥 Firewall: Nur Port 443 erforderlich") app_logger.info("🛡️ SSL-Zertifikate: Browser-kompatibel") # Starte Flask-Server mit SSL app.run( host=host, port=port, ssl_context=ssl_context, threaded=True, debug=False, use_reloader=False ) except PermissionError: app_logger.error("❌ Berechtigung verweigert für Port 443") if platform.system() != 'Windows': app_logger.error("💡 Führe aus mit: sudo python3 app_production.py") else: app_logger.error("💡 Führe als Administrator aus") sys.exit(1) except OSError as e: if "Address already in use" in str(e): app_logger.error("❌ Port 443 bereits belegt") app_logger.error("💡 Stoppe andere Services: sudo systemctl stop apache2 nginx") else: app_logger.error(f"❌ Netzwerk-Fehler: {e}") sys.exit(1) except Exception as e: app_logger.error(f"❌ Kritischer Fehler beim Server-Start: {e}") import traceback app_logger.error(f"Traceback: {traceback.format_exc()}") sys.exit(1) finally: # Cleanup try: from app import stop_queue_manager, cleanup_rate_limiter stop_queue_manager() if 'scheduler' in locals() and scheduler: scheduler.shutdown() cleanup_rate_limiter() app_logger.info("✅ Cleanup abgeschlossen") except: pass if __name__ == "__main__": main()