426 lines
14 KiB
Python
426 lines
14 KiB
Python
#!/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() |