#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ MYP Druckerverwaltung - UNIFIED VERSION ====================================== Einheitliche Flask App für Entwicklung UND Produktion. Diese App ersetzt sowohl app.py als auch app_production.py. Verwendung: - Development: python app_unified.py - Production: sudo python app_unified.py --production - SSL-Force: python app_unified.py --ssl Version: 6.0.0 Unified """ import os import sys import ssl import logging import platform import argparse 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-Logik from app import app, app_logger, init_database, create_initial_admin, main as app_main from app import start_queue_manager, stop_queue_manager, get_job_scheduler, cleanup_rate_limiter # Flask-Imports für Request-Handling from flask import request, redirect # =========================== UMGEBUNGS-ERKENNUNG =========================== def detect_environment(): """Erkennt automatisch die Laufzeitumgebung""" # Kommandozeilen-Argumente prüfen if '--production' in sys.argv or '--prod' in sys.argv: return 'production' if '--development' in sys.argv or '--dev' in sys.argv: return 'development' # Umgebungsvariablen prüfen env_mode = os.getenv('MYP_MODE', '').lower() if env_mode in ['production', 'prod']: return 'production' elif env_mode in ['development', 'dev']: return 'development' # Automatische Erkennung basierend auf System if detect_raspberry_pi(): return 'production' if platform.system() == 'Windows': return 'development' # Standard: Development für unbekannte Systeme return 'development' def detect_raspberry_pi(): """Erkennt ob das System auf einem Raspberry Pi läuft""" try: with open('/proc/cpuinfo', 'r') as f: cpuinfo = f.read() if 'Raspberry Pi' in cpuinfo or 'BCM' in cpuinfo: return True except: pass try: machine = platform.machine().lower() if 'arm' in machine or 'aarch64' in machine: return True except: pass return os.getenv('FORCE_RASPBERRY_PI', '').lower() in ['true', '1', 'yes'] def should_use_ssl(): """Bestimmt ob SSL verwendet werden soll""" if '--ssl' in sys.argv or '--https' in sys.argv: return True if '--no-ssl' in sys.argv or '--http' in sys.argv: return False env_ssl = os.getenv('MYP_SSL', '').lower() if env_ssl in ['true', '1', 'yes', 'force']: return True elif env_ssl in ['false', '0', 'no', 'disable']: return False # Automatisch: SSL für Production, HTTP für Development return detect_environment() == 'production' # =========================== KONFIGURATIONSKLASSEN =========================== class DevelopmentConfig: """Konfiguration für Entwicklungsumgebung""" # Debug-Einstellungen DEBUG = True TESTING = False # HTTP-Konfiguration FORCE_HTTPS = False SSL_REQUIRED = False HTTP_PORT = 5000 # Performance (weniger optimiert für bessere Debug-Möglichkeiten) OPTIMIZED_MODE = False USE_MINIFIED_ASSETS = False DISABLE_ANIMATIONS = False # Session-Konfiguration (weniger restriktiv für Development) SESSION_COOKIE_SECURE = False SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' # Reload-Features für Development TEMPLATES_AUTO_RELOAD = True EXPLAIN_TEMPLATE_LOADING = False class ProductionConfig: """Konfiguration für Produktionsumgebung""" # Produktions-Einstellungen DEBUG = False TESTING = False # HTTPS-Only Konfiguration FORCE_HTTPS = True SSL_REQUIRED = True HTTPS_PORT = 443 # Performance-Optimierungen 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 # Template-Optimierungen TEMPLATES_AUTO_RELOAD = False EXPLAIN_TEMPLATE_LOADING = False # SSL-Konfiguration SSL_CERT_PATH = None # Wird automatisch erkannt SSL_KEY_PATH = None # Wird automatisch erkannt # =========================== SSL-SETUP =========================== def get_ssl_paths(): """Ermittelt die SSL-Zertifikat-Pfade plattformspezifisch""" if platform.system() == 'Windows': ssl_dir = os.path.join(os.path.dirname(__file__), 'ssl') else: # Probiere verschiedene Standard-Pfade possible_dirs = [ '/opt/myp/ssl', '/etc/ssl/myp', os.path.join(os.path.dirname(__file__), 'ssl'), './ssl' ] ssl_dir = None for dir_path in possible_dirs: if os.path.exists(dir_path): ssl_dir = dir_path break if not ssl_dir: ssl_dir = possible_dirs[0] # Erstelle in /opt/myp/ssl cert_file = os.path.join(ssl_dir, 'cert.pem') key_file = os.path.join(ssl_dir, 'key.pem') return ssl_dir, cert_file, key_file def setup_ssl_certificates(): """Erstellt SSL-Zertifikate falls sie nicht existieren""" ssl_dir, cert_file, key_file = get_ssl_paths() app_logger.info(f"🔐 Prüfe SSL-Zertifikate in: {ssl_dir}") # Erstelle SSL-Verzeichnis os.makedirs(ssl_dir, exist_ok=True) # Prüfe ob Zertifikate existieren if os.path.exists(cert_file) and os.path.exists(key_file): try: # Teste Zertifikat-Gültigkeit context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(cert_file, key_file) app_logger.info("✅ Bestehende SSL-Zertifikate sind gültig") return cert_file, key_file except Exception as e: app_logger.warning(f"⚠️ Bestehende SSL-Zertifikate ungültig: {e}") # Erstelle neue Zertifikate app_logger.info("🔧 Erstelle neue SSL-Zertifikate...") try: # Versuche existierende SSL-Utilities zu verwenden if os.path.exists('./ssl/ssl_fix.py'): try: import subprocess result = subprocess.run([ sys.executable, './ssl/ssl_fix.py' ], capture_output=True, text=True, timeout=60) if result.returncode == 0: app_logger.info("✅ SSL-Zertifikate mit ssl_fix.py erstellt") return cert_file, key_file except Exception as e: app_logger.warning(f"⚠️ ssl_fix.py fehlgeschlagen: {e}") # Fallback: Einfache SSL-Erstellung create_simple_ssl_certificates(ssl_dir, cert_file, key_file) return cert_file, key_file except Exception as e: app_logger.error(f"❌ SSL-Zertifikat-Erstellung fehlgeschlagen: {e}") raise Exception(f"SSL-Setup fehlgeschlagen: {e}") def create_simple_ssl_certificates(ssl_dir, cert_file, key_file): """Erstellt einfache selbstsignierte SSL-Zertifikate""" try: # Versuche mit Python Cryptography Library from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa import ipaddress app_logger.info("🐍 Erstelle SSL-Zertifikate mit Python Cryptography...") # Private Key generieren private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, ) # Subject und Issuer subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, "DE"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Baden-Wuerttemberg"), x509.NameAttribute(NameOID.LOCALITY_NAME, "Stuttgart"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Mercedes-Benz AG"), x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "MYP Druckerverwaltung"), x509.NameAttribute(NameOID.COMMON_NAME, platform.node()), ]) # Subject Alternative Names san_list = [ x509.DNSName("localhost"), x509.DNSName("127.0.0.1"), x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), x509.DNSName(platform.node()), ] # Zertifikat erstellen cert = x509.CertificateBuilder().subject_name( subject ).issuer_name( issuer ).public_key( private_key.public_key() ).serial_number( x509.random_serial_number() ).not_valid_before( datetime.now() ).not_valid_after( datetime.now() + timedelta(days=365) ).add_extension( x509.SubjectAlternativeName(san_list), critical=False, ).sign(private_key, hashes.SHA256()) # Private Key schreiben with open(key_file, 'wb') as f: f.write(private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() )) # Zertifikat schreiben with open(cert_file, 'wb') as f: f.write(cert.public_bytes(serialization.Encoding.PEM)) # Berechtigungen setzen (Unix) try: os.chmod(cert_file, 0o644) os.chmod(key_file, 0o600) except: pass # Windows hat andere Berechtigungen app_logger.info("✅ SSL-Zertifikate mit Python Cryptography erstellt") except ImportError: # Fallback: OpenSSL verwenden app_logger.info("🔧 Erstelle SSL-Zertifikate mit OpenSSL...") import subprocess # Private Key erstellen subprocess.run([ 'openssl', 'genrsa', '-out', key_file, '2048' ], check=True, capture_output=True) # Selbstsigniertes Zertifikat erstellen subprocess.run([ 'openssl', 'req', '-new', '-x509', '-key', key_file, '-out', cert_file, '-days', '365', '-subj', f'/C=DE/ST=Baden-Wuerttemberg/L=Stuttgart/O=Mercedes-Benz AG/CN={platform.node()}' ], check=True, capture_output=True) app_logger.info("✅ SSL-Zertifikate mit OpenSSL erstellt") def get_ssl_context(): """Erstellt SSL-Kontext mit Zertifikaten""" if not should_use_ssl(): return None try: cert_file, key_file = setup_ssl_certificates() # SSL-Kontext erstellen context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(cert_file, key_file) # Sichere SSL-Einstellungen 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 app_logger.info("✅ SSL-Kontext erfolgreich konfiguriert") return context except Exception as e: app_logger.error(f"❌ SSL-Kontext-Erstellung fehlgeschlagen: {e}") app_logger.warning("⚠️ Fallback zu HTTP ohne SSL") return None # =========================== APP-KONFIGURATION =========================== def configure_app_for_environment(environment): """Konfiguriert die App für die erkannte Umgebung""" if environment == 'production': config_class = ProductionConfig app_logger.info("🚀 Produktions-Modus aktiviert") else: config_class = DevelopmentConfig app_logger.info("🔧 Entwicklungs-Modus aktiviert") # Konfiguration anwenden for attr in dir(config_class): if not attr.startswith('_'): app.config[attr] = getattr(config_class, attr) # Jinja-Globals setzen app.jinja_env.globals.update({ 'environment': environment, 'optimized_mode': config_class.OPTIMIZED_MODE, 'use_minified_assets': config_class.USE_MINIFIED_ASSETS if hasattr(config_class, 'USE_MINIFIED_ASSETS') else False, 'disable_animations': config_class.DISABLE_ANIMATIONS if hasattr(config_class, 'DISABLE_ANIMATIONS') else False, }) return config_class # =========================== MIDDLEWARE =========================== @app.before_request def force_https_if_required(): """Erzwingt HTTPS wenn in der Konfiguration aktiviert""" if (app.config.get('FORCE_HTTPS', False) and not request.is_secure and not request.headers.get('X-Forwarded-Proto') == 'https'): # Redirect zu HTTPS url = request.url.replace('http://', 'https://', 1) if ':5000' in url: url = url.replace(':5000', ':443') elif ':80' in url: url = url.replace(':80', ':443') return redirect(url, code=301) @app.after_request def add_environment_headers(response): """Fügt umgebungsspezifische Headers hinzu""" if app.config.get('FORCE_HTTPS', False): # Produktions-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' # Cache-Headers für statische Dateien if request.endpoint == 'static' or '/static/' in request.path: if app.config.get('OPTIMIZED_MODE', False): response.headers['Cache-Control'] = 'public, max-age=31536000' else: response.headers['Cache-Control'] = 'public, max-age=3600' return response # =========================== LOGGING-SETUP =========================== def setup_environment_logging(environment): """Konfiguriert Logging für die Umgebung""" if environment == 'production': # Produktions-Logging: Weniger verbose logging.getLogger('werkzeug').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) app_logger.setLevel(logging.INFO) # Entferne Debug-Handler for handler in app_logger.handlers[:]: if handler.level == logging.DEBUG: app_logger.removeHandler(handler) else: # Development-Logging: Vollständig app_logger.setLevel(logging.DEBUG) app_logger.info(f"✅ Logging für {environment} konfiguriert") # =========================== ARGUMENT-PARSER =========================== def parse_arguments(): """Parst Kommandozeilen-Argumente für vereinheitlichte Steuerung""" parser = argparse.ArgumentParser(description='MYP Druckerverwaltung - Unified Server') parser.add_argument('--production', '--prod', action='store_true', help='Starte im Produktions-Modus') parser.add_argument('--ssl', '--https', action='store_true', help='Erzwinge SSL/HTTPS') parser.add_argument('--port', type=int, default=None, help='Port-Nummer') return parser.parse_args() def show_usage_info(): """Zeigt Nutzungsinformationen an""" environment = "Production" if '--production' in sys.argv else "Development" ssl_enabled = '--ssl' in sys.argv or '--production' in sys.argv app_logger.info("🎯 MYP Unified App - Eine einzige funktionale App!") app_logger.info(f"📋 Modus: {environment}") app_logger.info(f"🔐 SSL: {'Aktiviert' if ssl_enabled else 'Deaktiviert'}") app_logger.info(f"💻 Plattform: {platform.system()}") app_logger.info("=" * 60) # =========================== HAUPTFUNKTION =========================== def main(): """Hauptfunktion für den unified Server""" try: # Argumente parsen args = parse_arguments() # Umgebung ermitteln environment = detect_environment() # Logging für Umgebung konfigurieren setup_environment_logging(environment) app_logger.info("🚀 MYP Unified 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}") app_logger.info(f"🌍 Umgebung: {environment}") app_logger.info(f"💻 Plattform: {platform.system()} {platform.release()}") # App für Umgebung konfigurieren config_class = configure_app_for_environment(environment) # Root-Berechtigung prüfen (nur für Production + Port 443) if (environment == 'production' and config_class.HTTPS_PORT == 443 and 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 python app_unified.py --production") sys.exit(1) elif platform.system() == 'Windows' and environment == 'production': app_logger.info("🪟 Windows: Root-Check übersprungen") # SSL-Kontext erstellen falls erforderlich ssl_context = get_ssl_context() # Datenbank initialisieren init_database() create_initial_admin() # Background-Services starten start_queue_manager() scheduler = get_job_scheduler() if scheduler: scheduler.start() app_logger.info("✅ Job-Scheduler gestartet") # Server-Konfiguration if args.port: port = args.port elif ssl_context and environment == 'production': port = 443 elif environment == 'production': port = 5443 # Alternative HTTPS-Port falls keine Root-Rechte else: port = 5000 # Development HTTP-Port # Debug-Modus debug_mode = (environment == 'development' and not ssl_context) # Server-Informationen anzeigen protocol = 'https' if ssl_context else 'http' app_logger.info(f"🌐 Server läuft auf: {protocol}://{platform.node()}:{port}") if platform.system() == 'Windows': app_logger.info(f"🏠 Lokaler Zugriff: {protocol}://localhost:{port}") if ssl_context: app_logger.info("🔐 SSL/HTTPS aktiviert") else: app_logger.info("🔓 HTTP-Modus (unverschlüsselt)") # Flask-Server starten app.run( host=platform.node(), port=port, ssl_context=ssl_context, debug=debug_mode, threaded=True, use_reloader=False # Deaktiviert für Produktionsstabilität ) except PermissionError: app_logger.error("❌ Berechtigung verweigert") if platform.system() != 'Windows': app_logger.error("💡 Führe als Root aus: sudo python app_unified.py --production") 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 bereits belegt") app_logger.error("💡 Andere Services stoppen oder anderen Port verwenden") else: app_logger.error(f"❌ Netzwerk-Fehler: {e}") sys.exit(1) except KeyboardInterrupt: app_logger.info("🛑 Server durch Benutzer gestoppt") sys.exit(0) 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: stop_queue_manager() if 'scheduler' in locals() and scheduler: scheduler.shutdown() cleanup_rate_limiter() app_logger.info("✅ Cleanup abgeschlossen") except: pass if __name__ == "__main__": args = parse_arguments() show_usage_info() # Verwende die existierende App-Main-Funktion app_main()