#!/usr/bin/env python3 """ Personal Internet Cell API Server Provides REST API endpoints for managing: - Cell status and configuration - Network services (DNS, DHCP, NTP) - WireGuard VPN and peer management - Email, Calendar, and File services - Routing and VPN gateway - Vault and trust management (Phase 6) """ import os import json import logging from datetime import datetime from flask import Flask, request, jsonify, current_app from flask_cors import CORS import threading import time from collections import deque import json as pyjson from logging.handlers import RotatingFileHandler import uuid import contextvars # Track API start time for uptime calculation API_START_TIME = time.time() from network_manager import NetworkManager from wireguard_manager import WireGuardManager from peer_registry import PeerRegistry from email_manager import EmailManager from calendar_manager import CalendarManager from file_manager import FileManager from routing_manager import RoutingManager from cell_manager import CellManager from vault_manager import VaultManager from container_manager import ContainerManager from config_manager import ConfigManager from service_bus import ServiceBus, EventType from log_manager import LogManager import firewall_manager # Context variable for request info request_context = contextvars.ContextVar('request_context', default={}) # Set default log level and log file if not already defined LOG_LEVEL = globals().get('LOG_LEVEL', 'INFO') LOG_FILE = globals().get('LOG_FILE', 'picell.log') class ContextFilter(logging.Filter): def filter(self, record): ctx = request_context.get({}) for k, v in ctx.items(): setattr(record, k, v) return True class JsonFormatter(logging.Formatter): def format(self, record): log_record = { 'timestamp': self.formatTime(record, self.datefmt), 'level': record.levelname, 'name': record.name, 'message': record.getMessage(), 'request_id': getattr(record, 'request_id', None), 'client_ip': getattr(record, 'client_ip', None), 'method': getattr(record, 'method', None), 'path': getattr(record, 'path', None), 'status': getattr(record, 'status', None), 'user': getattr(record, 'user', None), } if record.exc_info: log_record['exception'] = self.formatException(record.exc_info) return pyjson.dumps({k: v for k, v in log_record.items() if v is not None}) json_formatter = JsonFormatter() context_filter = ContextFilter() handlers = [logging.StreamHandler()] try: file_handler = RotatingFileHandler(LOG_FILE, maxBytes=5_000_000, backupCount=5, encoding='utf-8') file_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO)) file_handler.setFormatter(json_formatter) file_handler.addFilter(context_filter) handlers.append(file_handler) except Exception as e: print(f"Warning: Could not create rotating log file handler: {e}") for h in handlers: h.setFormatter(json_formatter) h.addFilter(context_filter) logging.basicConfig( level=getattr(logging, LOG_LEVEL, logging.INFO), handlers=handlers ) logger = logging.getLogger('picell') # Flask app setup app = Flask(__name__) CORS(app) # Development mode flag app.config['DEVELOPMENT_MODE'] = True # Set to True for development, False for production # Initialize enhanced components config_manager = ConfigManager(config_file='./config/cell_config.json', data_dir='./data') service_bus = ServiceBus() log_manager = LogManager(log_dir='./data/logs') # Initialize service loggers service_log_configs = { 'network': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'wireguard': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'email': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'calendar': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'files': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'routing': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'vault': {'level': 'INFO', 'formatter': 'json', 'console': False}, 'api': {'level': 'INFO', 'formatter': 'json', 'console': True} } for service, config in service_log_configs.items(): log_manager.add_service_logger(service, config) # Start service bus service_bus.start() @app.before_request def enrich_log_context(): req_id = str(uuid.uuid4()) client_ip = request.remote_addr method = request.method path = request.path user = getattr(getattr(request, 'user', None), 'id', None) or 'anonymous' request_context.set({ 'request_id': req_id, 'client_ip': client_ip, 'method': method, 'path': path, 'user': user }) @app.after_request def log_request(response): ctx = request_context.get({}) ctx['status'] = response.status_code logger.info(f"{ctx.get('method')} {ctx.get('path')} {ctx.get('status')}") return response @app.teardown_request def clear_log_context(exc): request_context.set({}) # Initialize managers — paths configurable via env for testing _DATA_DIR = os.environ.get('DATA_DIR', '/app/data') _CONFIG_DIR = os.environ.get('CONFIG_DIR', '/app/config') network_manager = NetworkManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) wireguard_manager = WireGuardManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) peer_registry = PeerRegistry(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) email_manager = EmailManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) calendar_manager = CalendarManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) file_manager = FileManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) routing_manager = RoutingManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) cell_manager = CellManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) app.vault_manager = VaultManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) container_manager = ContainerManager(data_dir=_DATA_DIR, config_dir=_CONFIG_DIR) # Apply firewall + DNS rules from stored peer settings (survives API restarts) def _apply_startup_enforcement(): try: peers = peer_registry.list_peers() firewall_manager.apply_all_peer_rules(peers) firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH) logger.info(f"Applied enforcement rules for {len(peers)} peers on startup") except Exception as e: logger.warning(f"Startup enforcement failed (non-fatal): {e}") COREFILE_PATH = '/app/config/dns/Corefile' # Run in background so startup isn't blocked waiting on docker exec threading.Thread(target=_apply_startup_enforcement, daemon=True).start() # Register services with service bus service_bus.register_service('network', network_manager) service_bus.register_service('wireguard', wireguard_manager) service_bus.register_service('email', email_manager) service_bus.register_service('calendar', calendar_manager) service_bus.register_service('files', file_manager) service_bus.register_service('routing', routing_manager) service_bus.register_service('vault', app.vault_manager) service_bus.register_service('container', container_manager) # Unified health monitoring HEALTH_HISTORY_SIZE = 100 health_history = deque(maxlen=HEALTH_HISTORY_SIZE) health_monitor_running = True # Health alerting configuration HEALTH_ALERT_THRESHOLD = 3 # Number of consecutive failures before alert service_alert_counters = {} def perform_health_check(): """Perform a unified health check of all services, with alerting.""" try: # Use service bus to get health from all services result = { 'timestamp': datetime.utcnow().isoformat(), 'alerts': [] } # Get health from each service for service_name in service_bus.list_services(): try: service = service_bus.get_service(service_name) if hasattr(service, 'health_check'): health = service.health_check() else: health = service.get_status() result[service_name] = health except Exception as e: result[service_name] = {'error': str(e), 'status': 'offline'} # Health alerting logic - improved to be more robust global service_alert_counters for service_name in service_bus.list_services(): if service_name in result: status = result[service_name] healthy = True # Improved health determination logic if isinstance(status, dict): # Check for explicit healthy field first if 'healthy' in status: healthy = status['healthy'] # Check for running status elif 'running' in status: healthy = status['running'] # Check for status field with various healthy values elif 'status' in status: status_value = status['status'] if isinstance(status_value, str): healthy = status_value.lower() in ('ok', 'healthy', 'online', 'active') else: healthy = bool(status_value) # Check for error field elif 'error' in status: healthy = False # If no health indicators, assume healthy if service exists else: healthy = True else: # If status is not a dict, assume it's a boolean healthy = bool(status) # Only count as unhealthy if we're certain it's down if not healthy: service_alert_counters[service_name] = service_alert_counters.get(service_name, 0) + 1 if service_alert_counters[service_name] >= HEALTH_ALERT_THRESHOLD: alert_msg = f"ALERT: {service_name} unhealthy for {service_alert_counters[service_name]} consecutive checks." logger.warning(alert_msg) result['alerts'].append(alert_msg) # Publish alert event service_bus.publish_event(EventType.ERROR_OCCURRED, service_name, { 'error': alert_msg, 'service': service_name, 'consecutive_failures': service_alert_counters[service_name] }) else: # Reset counter if service is healthy if service_alert_counters.get(service_name, 0) > 0: logger.info(f"Service {service_name} recovered, resetting alert counter") service_alert_counters[service_name] = 0 logger.info(f"Unified health check: {result}") return result except Exception as e: logger.error(f"Unified health check failed: {e}") return {'error': str(e), 'timestamp': datetime.utcnow().isoformat()} def health_monitor_loop(): while health_monitor_running: with app.app_context(): health_result = perform_health_check() health_history.appendleft(health_result) # Publish health check event service_bus.publish_event(EventType.HEALTH_CHECK, 'api', health_result) time.sleep(60) # Check every 60 seconds # Start health monitor thread health_monitor_thread = threading.Thread(target=health_monitor_loop, daemon=True) health_monitor_thread.start() def is_local_request(): # Allow requests from localhost, Docker networks, and internal IPs remote_addr = request.remote_addr forwarded_for = request.headers.get('X-Forwarded-For', '') # Check direct remote address if remote_addr in ('127.0.0.1', '::1', 'localhost'): return True # Check forwarded address (for reverse proxy scenarios) if forwarded_for: forwarded_ips = [ip.strip() for ip in forwarded_for.split(',')] for ip in forwarded_ips: if ip in ('127.0.0.1', '::1', 'localhost'): return True # Allow Docker internal networks (172.x.x.x, 192.168.x.x, 10.x.x.x) if remote_addr: try: import ipaddress ip = ipaddress.ip_address(remote_addr) if ip.is_private or ip.is_loopback: return True except: pass return False @app.route('/health', methods=['GET']) def health_check(): """Health check endpoint.""" try: return jsonify({ "status": "healthy", "timestamp": datetime.utcnow().isoformat(), "version": "1.0.0" }) except Exception as e: logger.error(f"Health check failed: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/status', methods=['GET']) def get_cell_status(): """Get overall cell status.""" try: # Use service bus to get status from all services services_status = {} for service_name in service_bus.list_services(): try: service = service_bus.get_service(service_name) services_status[service_name] = service.get_status() except Exception as e: services_status[service_name] = {'error': str(e)} peers = peer_registry.list_peers() # Calculate actual uptime current_time = time.time() uptime_seconds = int(current_time - API_START_TIME) return jsonify({ "cell_name": "personal-internet-cell", "domain": "cell.local", "uptime": uptime_seconds, "peers_count": len(peers), "services": services_status, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error getting cell status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/config', methods=['GET']) def get_config(): """Get cell configuration.""" try: service_configs = config_manager.get_all_configs() config = { 'cell_name': os.environ.get('CELL_NAME', 'mycell'), 'domain': os.environ.get('CELL_DOMAIN', 'cell'), 'ip_range': os.environ.get('CELL_IP_RANGE', '172.20.0.0/16'), 'wireguard_port': int(os.environ.get('WG_PORT', '51820')), } config.update(service_configs) return jsonify(config) except Exception as e: logger.error(f"Error getting config: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/config', methods=['PUT']) def update_config(): """Update cell configuration.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 # Update configuration using config manager for service, config in data.items(): if service in config_manager.service_schemas: success = config_manager.update_service_config(service, config) if success: # Publish config change event service_bus.publish_event(EventType.CONFIG_CHANGED, service, { 'service': service, 'config': config }) logger.info(f"Updated config: {data}") return jsonify({"message": "Configuration updated successfully"}) except Exception as e: logger.error(f"Error updating config: {e}") return jsonify({"error": str(e)}), 500 # Configuration management endpoints @app.route('/api/config/backup', methods=['POST']) def create_config_backup(): """Create configuration backup.""" try: backup_id = config_manager.backup_config() service_bus.publish_event(EventType.BACKUP_CREATED, 'api', { 'backup_id': backup_id, 'timestamp': datetime.utcnow().isoformat() }) return jsonify({"backup_id": backup_id}) except Exception as e: logger.error(f"Error creating backup: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/config/backups', methods=['GET']) def list_config_backups(): """List available backups.""" try: backups = config_manager.list_backups() return jsonify(backups) except Exception as e: logger.error(f"Error listing backups: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/config/restore/', methods=['POST']) def restore_config(backup_id): """Restore configuration from backup.""" try: success = config_manager.restore_config(backup_id) if success: service_bus.publish_event(EventType.RESTORE_COMPLETED, 'api', { 'backup_id': backup_id, 'timestamp': datetime.utcnow().isoformat() }) return jsonify({"message": f"Configuration restored from backup: {backup_id}"}) else: return jsonify({"error": f"Failed to restore backup: {backup_id}"}), 500 except Exception as e: logger.error(f"Error restoring backup: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/config/export', methods=['GET']) def export_config(): """Export configuration.""" try: format = request.args.get('format', 'json') config_data = config_manager.export_config(format) return jsonify({"config": config_data, "format": format}) except Exception as e: logger.error(f"Error exporting config: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/config/import', methods=['POST']) def import_config(): """Import configuration.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 config_data = data.get('config') format = data.get('format', 'json') success = config_manager.import_config(config_data, format) if success: return jsonify({"message": "Configuration imported successfully"}) else: return jsonify({"error": "Failed to import configuration"}), 500 except Exception as e: logger.error(f"Error importing config: {e}") return jsonify({"error": str(e)}), 500 # Service bus endpoints @app.route('/api/services/bus/status', methods=['GET']) def get_service_bus_status(): """Get service bus status.""" try: return jsonify(service_bus.get_service_status_summary()) except Exception as e: logger.error(f"Error getting service bus status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/services/bus/events', methods=['GET']) def get_service_bus_events(): """Get service bus event history.""" try: event_type = request.args.get('type') source = request.args.get('source') limit = int(request.args.get('limit', 100)) events = service_bus.get_event_history( EventType(event_type) if event_type else None, source, limit ) # Convert events to serializable format serializable_events = [] for event in events: serializable_events.append({ 'event_id': event.event_id, 'event_type': event.event_type.value, 'source': event.source, 'data': event.data, 'timestamp': event.timestamp.isoformat() }) return jsonify(serializable_events) except Exception as e: logger.error(f"Error getting service bus events: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/services/bus/services//start', methods=['POST']) def start_service(service_name): """Start a service with orchestration.""" try: success = service_bus.orchestrate_service_start(service_name) if success: return jsonify({"message": f"Service {service_name} started successfully"}) else: return jsonify({"error": f"Failed to start service {service_name}"}), 500 except Exception as e: logger.error(f"Error starting service {service_name}: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/services/bus/services//stop', methods=['POST']) def stop_service(service_name): """Stop a service with orchestration.""" try: success = service_bus.orchestrate_service_stop(service_name) if success: return jsonify({"message": f"Service {service_name} stopped successfully"}) else: return jsonify({"error": f"Failed to stop service {service_name}"}), 500 except Exception as e: logger.error(f"Error stopping service {service_name}: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/services/bus/services//restart', methods=['POST']) def restart_service(service_name): """Restart a service with orchestration.""" try: success = service_bus.orchestrate_service_restart(service_name) if success: return jsonify({"message": f"Service {service_name} restarted successfully"}) else: return jsonify({"error": f"Failed to restart service {service_name}"}), 500 except Exception as e: logger.error(f"Error restarting service {service_name}: {e}") return jsonify({"error": str(e)}), 500 # Logging endpoints @app.route('/api/logs/services/', methods=['GET']) def get_service_logs(service): """Get logs for a specific service.""" try: level = request.args.get('level', 'INFO') lines = int(request.args.get('lines', 50)) logs = log_manager.get_service_logs(service, level, lines) return jsonify({"service": service, "logs": logs}) except Exception as e: logger.error(f"Error getting logs for {service}: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/logs/search', methods=['POST']) def search_logs(): """Search logs across all services.""" try: data = request.get_json(silent=True) or {} query = data.get('query', '') services = data.get('services') level = data.get('level') time_range = data.get('time_range') results = log_manager.search_logs(query, time_range, services, level) return jsonify({"results": results, "count": len(results)}) except Exception as e: logger.error(f"Error searching logs: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/logs/export', methods=['POST']) def export_logs(): """Export logs in specified format.""" try: data = request.get_json(silent=True) or {} format = data.get('format', 'json') filters = data.get('filters', {}) log_data = log_manager.export_logs(format, filters) return jsonify({"logs": log_data, "format": format}) except Exception as e: logger.error(f"Error exporting logs: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/logs/statistics', methods=['GET']) def get_log_statistics(): """Get log statistics.""" try: service = request.args.get('service') stats = log_manager.get_log_statistics(service) return jsonify(stats) except Exception as e: logger.error(f"Error getting log statistics: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/logs/rotate', methods=['POST']) def rotate_logs(): """Manually rotate logs.""" try: data = request.get_json(silent=True) or {} service = data.get('service') log_manager.rotate_logs(service) return jsonify({"message": "Logs rotated successfully"}) except Exception as e: logger.error(f"Error rotating logs: {e}") return jsonify({"error": str(e)}), 500 # Network Services API @app.route('/api/dns/records', methods=['GET']) def get_dns_records(): """Get DNS records.""" try: records = network_manager.get_dns_records() return jsonify(records) except Exception as e: logger.error(f"Error getting DNS records: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/dns/records', methods=['POST']) def add_dns_record(): """Add DNS record.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = network_manager.add_dns_record(data) return jsonify(result) except Exception as e: logger.error(f"Error adding DNS record: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/dns/records', methods=['DELETE']) def remove_dns_record(): """Remove DNS record.""" try: data = request.get_json(silent=True) result = network_manager.remove_dns_record(data) return jsonify(result) except Exception as e: logger.error(f"Error removing DNS record: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/dhcp/leases', methods=['GET']) def get_dhcp_leases(): """Get DHCP leases.""" try: leases = network_manager.get_dhcp_leases() return jsonify(leases) except Exception as e: logger.error(f"Error getting DHCP leases: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/dhcp/reservations', methods=['POST']) def add_dhcp_reservation(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = network_manager.add_dhcp_reservation(data) return jsonify(result) except Exception as e: logger.error(f"Error adding DHCP reservation: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/dhcp/reservations', methods=['DELETE']) def remove_dhcp_reservation(): """Remove DHCP reservation.""" try: data = request.get_json(silent=True) result = network_manager.remove_dhcp_reservation(data) return jsonify(result) except Exception as e: logger.error(f"Error removing DHCP reservation: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/ntp/status', methods=['GET']) def get_ntp_status(): """Get NTP status.""" try: status = network_manager.get_ntp_status() return jsonify(status) except Exception as e: logger.error(f"Error getting NTP status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/network/info', methods=['GET']) def get_network_info(): """Get general network info (interfaces, gateway, DNS, etc.)""" try: info = network_manager.get_network_info() return jsonify(info) except Exception as e: logger.error(f"Error getting network info: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/dns/status', methods=['GET']) def get_dns_status(): """Get DNS service status and summary info.""" try: status = network_manager.get_dns_status() return jsonify(status) except Exception as e: logger.error(f"Error getting DNS status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/network/test', methods=['POST']) def test_network(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = network_manager.test_connectivity(data) return jsonify(result) except Exception as e: logger.error(f"Error testing network: {e}") return jsonify({"error": str(e)}), 500 # WireGuard API @app.route('/api/wireguard/keys', methods=['GET']) def get_wireguard_keys(): """Get WireGuard keys.""" try: result = wireguard_manager.get_keys() return jsonify(result) except Exception as e: logger.error(f"Error getting WireGuard keys: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/keys/peer', methods=['POST']) def generate_peer_keys(): """Generate peer keys.""" try: data = request.get_json(silent=True) or {} name = data.get('name') or data.get('peer_name') if not name: return jsonify({"error": "Missing peer name"}), 400 result = wireguard_manager.generate_peer_keys(name) return jsonify(result) except Exception as e: logger.error(f"Error generating peer keys: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/config', methods=['GET']) def get_wireguard_config(): """Get WireGuard configuration.""" try: result = wireguard_manager.get_config() return jsonify(result) except Exception as e: logger.error(f"Error getting WireGuard config: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers', methods=['GET']) def get_wireguard_peers(): """Get WireGuard peers.""" try: peers = wireguard_manager.get_peers() return jsonify(peers) except Exception as e: logger.error(f"Error getting WireGuard peers: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers', methods=['POST']) def add_wireguard_peer(): """Add WireGuard peer.""" try: data = request.get_json(silent=True) or {} result = wireguard_manager.add_peer( name=data.get('name', ''), public_key=data.get('public_key', ''), endpoint_ip=data.get('endpoint', data.get('endpoint_ip', '')), allowed_ips=data.get('allowed_ips', ''), persistent_keepalive=data.get('persistent_keepalive', 25) ) return jsonify({"success": result}) except Exception as e: logger.error(f"Error adding WireGuard peer: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers', methods=['DELETE']) def remove_wireguard_peer(): """Remove WireGuard peer.""" try: data = request.get_json(silent=True) or {} public_key = data.get('public_key') or data.get('name', '') result = wireguard_manager.remove_peer(public_key) return jsonify({"success": result}) except Exception as e: logger.error(f"Error removing WireGuard peer: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/status', methods=['GET']) def get_wireguard_status(): """Get WireGuard status.""" try: status = wireguard_manager.get_status() return jsonify(status) except Exception as e: logger.error(f"Error getting WireGuard status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/connectivity', methods=['POST']) def test_wireguard_connectivity(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = wireguard_manager.test_connectivity(data) return jsonify(result) except Exception as e: logger.error(f"Error testing WireGuard connectivity: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers/ip', methods=['PUT']) def update_peer_ip(): """Update peer IP.""" try: data = request.get_json(silent=True) or {} result = wireguard_manager.update_peer_ip( data.get('public_key', data.get('peer', '')), data.get('ip', '') ) return jsonify({"success": result}) except Exception as e: logger.error(f"Error updating peer IP: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers/status', methods=['POST']) def get_peer_status(): """Get live WireGuard status for a single peer.""" try: data = request.get_json(silent=True) or {} public_key = data.get('public_key', '') if not public_key: return jsonify({"error": "Missing public_key"}), 400 status = wireguard_manager.get_peer_status(public_key) return jsonify(status) except Exception as e: logger.error(f"Error getting peer status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers/statuses', methods=['GET']) def get_all_peer_statuses(): """Get live WireGuard status for all peers (keyed by public_key).""" try: statuses = wireguard_manager.get_all_peer_statuses() return jsonify(statuses) except Exception as e: logger.error(f"Error getting peer statuses: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/network/setup', methods=['POST']) def setup_network(): """Setup network configuration for internet access.""" try: success = wireguard_manager.setup_network_configuration() if success: return jsonify({"message": "Network configuration setup completed successfully"}) else: return jsonify({"error": "Failed to setup network configuration"}), 500 except Exception as e: logger.error(f"Error setting up network configuration: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/network/status', methods=['GET']) def get_network_status(): """Get network configuration status.""" try: status = wireguard_manager.get_network_status() return jsonify(status) except Exception as e: logger.error(f"Error getting network status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/peers/config', methods=['POST']) def get_peer_config(): try: data = request.get_json(silent=True) or {} peer_name = data.get('name', data.get('peer', '')) # Look up peer details from registry if not supplied peer_ip = data.get('ip', '') peer_private_key = data.get('private_key', '') registered = peer_registry.get_peer(peer_name) if peer_name else {} if peer_name and (not peer_ip or not peer_private_key): if registered: peer_ip = peer_ip or registered.get('ip', '') peer_private_key = peer_private_key or registered.get('private_key', '') # Use real external endpoint if not supplied server_endpoint = data.get('server_endpoint', '') if not server_endpoint: srv = wireguard_manager.get_server_config() server_endpoint = srv.get('endpoint') or '' # Determine AllowedIPs: explicit > peer's stored internet_access > default full tunnel allowed_ips = data.get('allowed_ips') or None if not allowed_ips and registered: internet_access = registered.get('internet_access', True) allowed_ips = wireguard_manager.FULL_TUNNEL_IPS if internet_access else wireguard_manager.SPLIT_TUNNEL_IPS result = wireguard_manager.get_peer_config( peer_name=peer_name, peer_ip=peer_ip, peer_private_key=peer_private_key, server_endpoint=server_endpoint, allowed_ips=allowed_ips, ) return jsonify({"config": result}) except Exception as e: logger.error(f"Error getting peer config: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/server-config', methods=['GET']) def get_server_config(): try: config = wireguard_manager.get_server_config() return jsonify(config) except Exception as e: logger.error(f"Error getting server config: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/refresh-ip', methods=['POST']) def refresh_external_ip(): try: ip = wireguard_manager.get_external_ip(force_refresh=True) return jsonify({ 'external_ip': ip, 'port': 51820, 'endpoint': f'{ip}:51820' if ip else None, }) except Exception as e: logger.error(f"Error refreshing external IP: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/wireguard/apply-enforcement', methods=['POST']) def apply_wireguard_enforcement(): """Re-apply per-peer iptables and DNS enforcement rules (call after WireGuard restart).""" try: peers = peer_registry.list_peers() firewall_manager.apply_all_peer_rules(peers) firewall_manager.apply_all_dns_rules(peers, COREFILE_PATH) return jsonify({'ok': True, 'peers': len(peers)}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/wireguard/check-port', methods=['POST']) def check_wireguard_port(): try: port_open = wireguard_manager.check_port_open() return jsonify({'port_open': port_open, 'port': 51820}) except Exception as e: return jsonify({"error": str(e)}), 500 # Peer Registry API @app.route('/api/peers', methods=['GET']) def get_peers(): """Get all peers.""" try: peers = peer_registry.list_peers() return jsonify(peers) except Exception as e: logger.error(f"Error getting peers: {e}") return jsonify({"error": str(e)}), 500 def _next_peer_ip() -> str: """Auto-assign the next free 10.0.0.x address (starts at .2, skips .1 = server).""" import ipaddress used = {p.get('ip', '').split('/')[0] for p in peer_registry.list_peers()} network = ipaddress.ip_network('10.0.0.0/24') for host in network.hosts(): ip = str(host) if ip == '10.0.0.1': continue # server address if ip not in used: return ip raise ValueError('No free IPs left in 10.0.0.0/24') @app.route('/api/peers', methods=['POST']) def add_peer(): """Add a peer.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 # Validate required fields (ip is optional — auto-assigned if omitted) required_fields = ['name', 'public_key'] for field in required_fields: if field not in data: return jsonify({"error": f"Missing required field: {field}"}), 400 assigned_ip = data.get('ip') or _next_peer_ip() # Add peer to registry with all provided fields peer_info = { 'peer': data['name'], 'ip': assigned_ip, 'public_key': data['public_key'], 'private_key': data.get('private_key'), 'server_public_key': data.get('server_public_key'), 'server_endpoint': data.get('server_endpoint'), 'allowed_ips': data.get('allowed_ips'), 'persistent_keepalive': data.get('persistent_keepalive'), 'description': data.get('description'), 'internet_access': data.get('internet_access', True), 'service_access': data.get('service_access', ['calendar', 'files', 'mail', 'webdav']), 'peer_access': data.get('peer_access', True), 'config_needs_reinstall': False, } success = peer_registry.add_peer(peer_info) if success: # Apply server-side enforcement immediately firewall_manager.apply_peer_rules(peer_info['ip'], peer_info) firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH) return jsonify({"message": f"Peer {data['name']} added successfully", "ip": assigned_ip}), 201 else: return jsonify({"error": f"Peer {data['name']} already exists"}), 400 except Exception as e: logger.error(f"Error adding peer: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/peers/', methods=['PUT']) def update_peer(peer_name): """Update peer settings. Marks config_needs_reinstall if VPN config changed.""" try: data = request.get_json(silent=True) or {} existing = peer_registry.get_peer(peer_name) if not existing: return jsonify({"error": "Peer not found"}), 404 # Detect changes that require client to reinstall tunnel config config_changed = ( ('internet_access' in data and data['internet_access'] != existing.get('internet_access', True)) or ('ip' in data and data['ip'] != existing.get('ip')) or ('persistent_keepalive' in data and data['persistent_keepalive'] != existing.get('persistent_keepalive')) ) updates = {k: v for k, v in data.items()} if config_changed: updates['config_needs_reinstall'] = True success = peer_registry.update_peer(peer_name, updates) if success: # Re-apply server-side enforcement with updated settings updated_peer = peer_registry.get_peer(peer_name) if updated_peer: firewall_manager.apply_peer_rules(updated_peer['ip'], updated_peer) firewall_manager.apply_all_dns_rules(peer_registry.list_peers(), COREFILE_PATH) result = {"message": f"Peer {peer_name} updated", "config_changed": config_changed} return jsonify(result) else: return jsonify({"error": "Update failed"}), 500 except Exception as e: logger.error(f"Error updating peer {peer_name}: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/peers//clear-reinstall', methods=['POST']) def clear_peer_reinstall(peer_name): """Clear the config_needs_reinstall flag once user has downloaded new config.""" try: peer_registry.clear_reinstall_flag(peer_name) return jsonify({"message": "Reinstall flag cleared"}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/api/peers/', methods=['DELETE']) def remove_peer(peer_name): """Remove a peer.""" try: success = peer_registry.remove_peer(peer_name) if success: return jsonify({"message": f"Peer {peer_name} removed successfully"}) else: return jsonify({"message": f"Peer {peer_name} not found or already removed"}) except Exception as e: logger.error(f"Error removing peer: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/peers/register', methods=['POST']) def register_peer(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = peer_registry.register_peer(data) return jsonify(result) except Exception as e: logger.error(f"Error registering peer: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/peers//unregister', methods=['DELETE']) def unregister_peer(peer_name): """Unregister a peer.""" try: result = peer_registry.unregister_peer(peer_name) return jsonify(result) except Exception as e: logger.error(f"Error unregistering peer: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/peers//update-ip', methods=['PUT']) def update_peer_ip_registry(peer_name): """Update peer IP.""" try: data = request.get_json(silent=True) new_ip = data.get('ip') if data else None if not new_ip: return jsonify({"error": "Missing ip"}), 400 success = peer_registry.update_peer_ip(peer_name, new_ip) if success: # Update routing and WireGuard configs try: routing_manager.update_peer_ip(peer_name, new_ip) except Exception as e: logger.warning(f"RoutingManager update_peer_ip failed: {e}") try: # For now, skip WireGuard update - method not implemented logger.warning(f"WireGuardManager update_peer_ip not implemented yet") except Exception as e: logger.warning(f"WireGuardManager update_peer_ip failed: {e}") return jsonify({"message": f"IP update received for {peer_name}"}) else: return jsonify({"error": f"Peer {peer_name} not found"}), 404 except Exception as e: logger.error(f"Error updating peer IP: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/ip-update', methods=['POST']) def ip_update(): """Handle IP update from peer.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 peer_name = data.get('peer') new_ip = data.get('ip') if not peer_name or not new_ip: return jsonify({"error": "Missing peer or ip"}), 400 success = peer_registry.update_peer_ip(peer_name, new_ip) if success: # Update routing and WireGuard configs try: routing_manager.update_peer_ip(peer_name, new_ip) except Exception as e: logger.warning(f"RoutingManager update_peer_ip failed: {e}") try: # For now, skip WireGuard update - method not implemented logger.warning(f"WireGuardManager update_peer_ip not implemented yet") except Exception as e: logger.warning(f"WireGuardManager update_peer_ip failed: {e}") return jsonify({"message": f"IP update received for {peer_name}"}) else: return jsonify({"error": f"Peer {peer_name} not found"}), 404 except Exception as e: logger.error(f"Error handling IP update: {e}") return jsonify({"error": str(e)}), 500 # Email Services API @app.route('/api/email/users', methods=['GET']) def get_email_users(): """Get email users.""" try: users = email_manager.get_users() return jsonify(users) except Exception as e: logger.error(f"Error getting email users: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/email/users', methods=['POST']) def create_email_user(): """Create email user.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = email_manager.create_user(data) return jsonify(result) except Exception as e: logger.error(f"Error creating email user: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/email/users/', methods=['DELETE']) def delete_email_user(username): """Delete email user.""" try: result = email_manager.delete_user(username) return jsonify(result) except Exception as e: logger.error(f"Error deleting email user: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/email/status', methods=['GET']) def get_email_status(): """Get email service status.""" try: status = email_manager.get_status() return jsonify(status) except Exception as e: logger.error(f"Error getting email status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/email/connectivity', methods=['GET']) def test_email_connectivity(): """Test email connectivity.""" try: result = email_manager.test_connectivity() return jsonify(result) except Exception as e: logger.error(f"Error testing email connectivity: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/email/send', methods=['POST']) def send_email(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = email_manager.send_email(data) return jsonify(result) except Exception as e: logger.error(f"Error sending email: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/email/mailbox/', methods=['GET']) def get_mailbox_info(username): """Get mailbox information.""" try: result = email_manager.get_mailbox_info(username) return jsonify(result) except Exception as e: logger.error(f"Error getting mailbox info: {e}") return jsonify({"error": str(e)}), 500 # Calendar Services API @app.route('/api/calendar/users', methods=['GET']) def get_calendar_users(): """Get calendar users.""" try: users = calendar_manager.get_users() return jsonify(users) except Exception as e: logger.error(f"Error getting calendar users: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/users', methods=['POST']) def create_calendar_user(): """Create calendar user.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = calendar_manager.create_user(data) return jsonify(result) except Exception as e: logger.error(f"Error creating calendar user: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/users/', methods=['DELETE']) def delete_calendar_user(username): """Delete calendar user.""" try: result = calendar_manager.delete_user(username) return jsonify(result) except Exception as e: logger.error(f"Error deleting calendar user: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/calendars', methods=['POST']) def create_calendar(): """Create calendar.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = calendar_manager.create_calendar(data) return jsonify(result) except Exception as e: logger.error(f"Error creating calendar: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/events', methods=['POST']) def add_calendar_event(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = calendar_manager.add_event(data) return jsonify(result) except Exception as e: logger.error(f"Error adding calendar event: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/events//', methods=['GET']) def get_calendar_events(username, calendar_name): """Get calendar events.""" try: params = request.args.to_dict() result = calendar_manager.get_events(username, calendar_name, params) return jsonify(result) except Exception as e: logger.error(f"Error getting calendar events: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/status', methods=['GET']) def get_calendar_status(): """Get calendar service status.""" try: status = calendar_manager.get_status() return jsonify(status) except Exception as e: logger.error(f"Error getting calendar status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/calendar/connectivity', methods=['GET']) def test_calendar_connectivity(): """Test calendar connectivity.""" try: result = calendar_manager.test_connectivity() return jsonify(result) except Exception as e: logger.error(f"Error testing calendar connectivity: {e}") return jsonify({"error": str(e)}), 500 # File Services API @app.route('/api/files/users', methods=['GET']) def get_file_users(): """Get file storage users.""" try: users = file_manager.get_users() return jsonify(users) except Exception as e: logger.error(f"Error getting file users: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/users', methods=['POST']) def create_file_user(): """Create file storage user.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = file_manager.create_user(data) return jsonify(result) except Exception as e: logger.error(f"Error creating file user: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/users/', methods=['DELETE']) def delete_file_user(username): """Delete file storage user.""" try: result = file_manager.delete_user(username) return jsonify(result) except Exception as e: logger.error(f"Error deleting file user: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/folders', methods=['POST']) def create_folder(): """Create folder.""" try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = file_manager.create_folder(data) return jsonify(result) except Exception as e: logger.error(f"Error creating folder: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/folders//', methods=['DELETE']) def delete_folder(username, folder_path): """Delete folder.""" try: result = file_manager.delete_folder(username, folder_path) return jsonify(result) except Exception as e: logger.error(f"Error deleting folder: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/upload/', methods=['POST']) def upload_file(username): """Upload file.""" try: if 'file' not in request.files: return jsonify({"error": "No file provided"}), 400 file = request.files['file'] path = request.form.get('path', '') result = file_manager.upload_file(username, file, path) return jsonify(result) except Exception as e: logger.error(f"Error uploading file: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/download//', methods=['GET']) def download_file(username, file_path): """Download file.""" try: result = file_manager.download_file(username, file_path) return jsonify(result) except Exception as e: logger.error(f"Error downloading file: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/delete//', methods=['DELETE']) def delete_file(username, file_path): """Delete file.""" try: result = file_manager.delete_file(username, file_path) return jsonify(result) except Exception as e: logger.error(f"Error deleting file: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/list/', methods=['GET']) def list_files(username): """List files.""" try: folder = request.args.get('folder', '') result = file_manager.list_files(username, folder) return jsonify(result) except Exception as e: logger.error(f"Error listing files: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/status', methods=['GET']) def get_file_status(): """Get file service status.""" try: status = file_manager.get_status() return jsonify(status) except Exception as e: logger.error(f"Error getting file status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/files/connectivity', methods=['GET']) def test_file_connectivity(): """Test file service connectivity.""" try: result = file_manager.test_connectivity() return jsonify(result) except Exception as e: logger.error(f"Error testing file connectivity: {e}") return jsonify({"error": str(e)}), 500 # Routing API @app.route('/api/routing/status', methods=['GET']) def get_routing_status(): """Get routing status.""" try: status = routing_manager.get_status() return jsonify(status) except Exception as e: logger.error(f"Error getting routing status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/setup', methods=['POST']) def setup_routing(): """Apply/verify routing setup (WireGuard handles NAT via PostUp rules).""" try: status = routing_manager.get_status() return jsonify({'success': True, 'message': 'Routing managed by WireGuard PostUp rules', **status}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/api/routing/nat', methods=['POST']) def add_nat_rule(): """Add NAT rule. JSON fields: - source_network (CIDR) - target_interface (str) - masquerade (bool, default True) - nat_type (MASQUERADE, SNAT, DNAT) - protocol (TCP, UDP, ALL) - external_port (str, optional) - internal_ip (str, optional) - internal_port (str, optional) """ try: data = request.get_json(silent=True) or {} result = routing_manager.add_nat_rule( source_network=data.get('source_network'), target_interface=data.get('target_interface'), masquerade=data.get('masquerade', True), nat_type=data.get('nat_type', 'MASQUERADE'), protocol=data.get('protocol', 'ALL'), external_port=data.get('external_port'), internal_ip=data.get('internal_ip'), internal_port=data.get('internal_port') ) return jsonify({'success': result}) except Exception as e: logger.error(f"Error adding NAT rule: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/nat/', methods=['DELETE']) def remove_nat_rule(rule_id): """Remove NAT rule.""" try: result = routing_manager.remove_nat_rule(rule_id) return jsonify(result) except Exception as e: logger.error(f"Error removing NAT rule: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/peers', methods=['POST']) def add_peer_route(): """Add peer route.""" try: data = request.get_json(silent=True) result = routing_manager.add_peer_route(data) return jsonify(result) except Exception as e: logger.error(f"Error adding peer route: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/peers/', methods=['DELETE']) def remove_peer_route(peer_name): """Remove peer route.""" try: result = routing_manager.remove_peer_route(peer_name) return jsonify(result) except Exception as e: logger.error(f"Error removing peer route: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/exit-nodes', methods=['POST']) def add_exit_node(): """Add exit node.""" try: data = request.get_json(silent=True) result = routing_manager.add_exit_node(data) return jsonify(result) except Exception as e: logger.error(f"Error adding exit node: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/bridge', methods=['POST']) def add_bridge_route(): """Add bridge route.""" try: data = request.get_json(silent=True) result = routing_manager.add_bridge_route(data) return jsonify(result) except Exception as e: logger.error(f"Error adding bridge route: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/split', methods=['POST']) def add_split_route(): """Add split route.""" try: data = request.get_json(silent=True) result = routing_manager.add_split_route(data) return jsonify(result) except Exception as e: logger.error(f"Error adding split route: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/firewall', methods=['POST']) def add_firewall_rule(): """Add firewall rule. JSON fields: - rule_type (INPUT, OUTPUT, FORWARD) - source (CIDR) - destination (CIDR) - action (ACCEPT, DROP, REJECT) - protocol (TCP, UDP, ICMP, ALL) - port (str, optional) - port_range (str, optional, e.g. '1000-2000') """ try: data = request.get_json(silent=True) or {} result = routing_manager.add_firewall_rule( rule_type=data.get('rule_type'), source=data.get('source'), destination=data.get('destination'), action=data.get('action', 'ACCEPT'), port=data.get('port'), protocol=data.get('protocol', 'ALL'), port_range=data.get('port_range') ) return jsonify({'success': result}) except Exception as e: logger.error(f"Error adding firewall rule: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/firewall/', methods=['DELETE']) def remove_firewall_rule(rule_id): try: result = routing_manager.remove_firewall_rule(rule_id) return jsonify({'success': result}), (200 if result else 404) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/routing/live-iptables', methods=['GET']) def get_live_iptables(): try: return jsonify(routing_manager.get_live_iptables()) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/routing/connectivity', methods=['POST']) def test_routing_connectivity(): """Test routing connectivity.""" try: data = request.get_json(silent=True) result = routing_manager.test_connectivity(data) return jsonify(result) except Exception as e: logger.error(f"Error testing routing connectivity: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/logs', methods=['GET']) def get_routing_logs(): """Get routing logs.""" try: lines = request.args.get('lines', 50, type=int) result = routing_manager.get_logs(lines) return jsonify(result) except Exception as e: logger.error(f"Error getting routing logs: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/nat', methods=['GET']) def get_nat_rules(): """Get all NAT rules.""" try: rules = routing_manager.get_nat_rules() return jsonify({"nat_rules": rules}) except Exception as e: logger.error(f"Error getting NAT rules: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/peers', methods=['GET']) def get_peer_routes(): """Get all peer routes.""" try: routes = routing_manager.get_peer_routes() return jsonify({"peer_routes": routes}) except Exception as e: logger.error(f"Error getting peer routes: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/routing/firewall', methods=['GET']) def get_firewall_rules(): """Get all firewall rules.""" try: rules = routing_manager.get_firewall_rules() return jsonify({"firewall_rules": rules}) except Exception as e: logger.error(f"Error getting firewall rules: {e}") return jsonify({"error": str(e)}), 500 # Vault & Trust API (Phase 6) @app.route('/api/vault/status', methods=['GET']) def get_vault_status(): """Get vault status.""" try: status = current_app.vault_manager.get_status() return jsonify(status) except Exception as e: logger.error(f"Error getting vault status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/certificates', methods=['GET']) def get_certificates(): """Get all certificates.""" try: certificates = current_app.vault_manager.list_certificates() return jsonify(certificates) except Exception as e: logger.error(f"Error getting certificates: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/certificates', methods=['POST']) def generate_certificate(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = current_app.vault_manager.generate_certificate( common_name=data['common_name'], domains=data.get('domains', []), key_size=data.get('key_size', 2048), days=data.get('days', 365) ) return jsonify(result) except Exception as e: logger.error(f"Error generating certificate: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/certificates/', methods=['DELETE']) def revoke_certificate(common_name): """Revoke certificate.""" try: result = current_app.vault_manager.revoke_certificate(common_name) return jsonify({"revoked": result}) except Exception as e: logger.error(f"Error revoking certificate: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/ca/certificate', methods=['GET']) def get_ca_certificate(): """Get CA certificate.""" try: cert = current_app.vault_manager.get_ca_certificate() return jsonify({"certificate": cert}) except Exception as e: logger.error(f"Error getting CA certificate: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/age/public-key', methods=['GET']) def get_age_public_key(): """Get Age public key.""" try: key = current_app.vault_manager.get_age_public_key() return jsonify({"public_key": key}) except Exception as e: logger.error(f"Error getting Age public key: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/trust/keys', methods=['GET']) def get_trusted_keys(): """Get trusted keys.""" try: keys = current_app.vault_manager.get_trusted_keys() return jsonify(keys) except Exception as e: logger.error(f"Error getting trusted keys: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/trust/keys', methods=['POST']) def add_trusted_key(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = current_app.vault_manager.add_trusted_key( name=data['name'], public_key=data['public_key'], trust_level=data.get('trust_level', 'direct') ) return jsonify({"added": result}) except Exception as e: logger.error(f"Error adding trusted key: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/trust/keys/', methods=['DELETE']) def remove_trusted_key(name): """Remove trusted key.""" try: result = current_app.vault_manager.remove_trusted_key(name) return jsonify({"removed": result}) except Exception as e: logger.error(f"Error removing trusted key: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/trust/verify', methods=['POST']) def verify_trust_chain(): try: data = request.get_json(silent=True) if data is None: return jsonify({"error": "No data provided"}), 400 result = current_app.vault_manager.verify_trust_chain( peer_name=data['peer_name'], signature=data['signature'], data=data['data'] ) return jsonify({"verified": result}) except Exception as e: logger.error(f"Error verifying trust chain: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/vault/trust/chains', methods=['GET']) def get_trust_chains(): """Get trust chains.""" try: chains = current_app.vault_manager.get_trust_chains() return jsonify(chains) except Exception as e: logger.error(f"Error getting trust chains: {e}") return jsonify({"error": str(e)}), 500 # Services API @app.route('/api/services/status', methods=['GET']) def get_all_services_status(): """Get status of all services.""" try: # Use service bus to get status from all services services_status = {} for service_name in service_bus.list_services(): try: service = service_bus.get_service(service_name) status = service.get_status() # Clean up status for UI consumption if isinstance(status, dict): # Extract core status information clean_status = { 'status': status.get('status', 'unknown'), 'running': status.get('running', False), 'timestamp': status.get('timestamp', datetime.utcnow().isoformat()) } # Add service-specific metrics if service_name == 'network': clean_status.update({ 'dns_status': status.get('dns_running', False), 'dhcp_status': status.get('dhcp_running', False), 'ntp_status': status.get('ntp_running', False) }) elif service_name == 'wireguard': clean_status.update({ 'peers_count': status.get('peers_count', 0), 'interface': status.get('interface', 'unknown') }) elif service_name == 'email': clean_status.update({ 'users_count': status.get('users_count', 0), 'domain': status.get('domain', 'unknown') }) elif service_name == 'calendar': clean_status.update({ 'users_count': status.get('users_count', 0), 'calendars_count': status.get('calendars_count', 0) }) elif service_name == 'files': clean_status.update({ 'users_count': status.get('users_count', 0), 'storage_used': status.get('total_storage_used', {}) }) elif service_name == 'routing': clean_status.update({ 'nat_rules_count': status.get('nat_rules_count', 0), 'peer_routes_count': status.get('peer_routes_count', 0), 'firewall_rules_count': status.get('firewall_rules_count', 0) }) elif service_name == 'vault': clean_status.update({ 'certificates_count': status.get('certificates_count', 0), 'trusted_keys_count': status.get('trusted_keys_count', 0) }) services_status[service_name] = clean_status else: services_status[service_name] = {'status': str(status), 'running': bool(status)} except Exception as e: services_status[service_name] = {'error': str(e), 'status': 'offline', 'running': False} return jsonify({ "network": services_status.get('network', {}), "wireguard": services_status.get('wireguard', {}), "email": services_status.get('email', {}), "calendar": services_status.get('calendar', {}), "files": services_status.get('files', {}), "routing": services_status.get('routing', {}), "vault": services_status.get('vault', {}), "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error getting all services status: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/services/connectivity', methods=['GET']) def test_all_services_connectivity(): """Test connectivity of all services.""" try: # Use service bus to test connectivity connectivity_results = {} for service_name in service_bus.list_services(): try: service = service_bus.get_service(service_name) if hasattr(service, 'test_connectivity'): connectivity_results[service_name] = service.test_connectivity() else: connectivity_results[service_name] = {'status': 'ok', 'message': 'No connectivity test available'} except Exception as e: connectivity_results[service_name] = {'status': 'error', 'message': str(e)} return jsonify({ "network": connectivity_results.get('network', {}), "wireguard": connectivity_results.get('wireguard', {}), "email": connectivity_results.get('email', {}), "calendar": connectivity_results.get('calendar', {}), "files": connectivity_results.get('files', {}), "routing": connectivity_results.get('routing', {}), "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error testing all services connectivity: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/health/history', methods=['GET']) def get_health_history(): """Get recent unified health check results.""" return jsonify(list(health_history)) @app.route('/api/logs', methods=['GET']) def get_backend_logs(): """Get backend log file contents (last N lines).""" log_file = os.path.join(os.path.dirname(__file__), 'picell.log') lines = int(request.args.get('lines', 100)) try: if not os.path.exists(log_file): return jsonify({"error": "Log file not found."}), 404 with open(log_file, 'r', encoding='utf-8', errors='ignore') as f: all_lines = f.readlines() tail_lines = all_lines[-lines:] if lines > 0 else all_lines return jsonify({"log": ''.join(tail_lines)}) except Exception as e: logger.error(f"Error reading log file: {e}") return jsonify({"error": str(e)}), 500 @app.route('/api/containers', methods=['GET']) def list_containers(): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 try: containers = container_manager.list_containers() return jsonify(containers) except Exception as e: logger.error(f"Error listing containers: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/containers//start', methods=['POST']) def start_container(name): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 try: success = container_manager.start_container(name) return jsonify({'started': success}) except Exception as e: logger.error(f"Error starting container {name}: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/containers//stop', methods=['POST']) def stop_container(name): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 try: success = container_manager.stop_container(name) return jsonify({'stopped': success}) except Exception as e: logger.error(f"Error stopping container {name}: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/containers//restart', methods=['POST']) def restart_container(name): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 try: success = container_manager.restart_container(name) return jsonify({'restarted': success}) except Exception as e: logger.error(f"Error restarting container {name}: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/containers//logs', methods=['GET']) def get_container_logs(name): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 tail = request.args.get('tail', default=100, type=int) try: logs = container_manager.get_container_logs(name, tail=tail) return jsonify({'logs': logs}) except Exception as e: logger.error(f"Error getting logs for container {name}: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/containers//stats', methods=['GET']) def get_container_stats(name): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 try: stats = container_manager.get_container_stats(name) return jsonify(stats) except Exception as e: logger.error(f"Error getting stats for container {name}: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/vault/secrets', methods=['GET']) def list_secrets(): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 secrets = app.vault_manager.list_secrets() return jsonify({'secrets': secrets}) @app.route('/api/vault/secrets', methods=['POST']) def store_secret(): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 data = request.get_json(silent=True) if not data or 'name' not in data or 'value' not in data: return jsonify({'error': 'Missing name or value'}), 400 app.vault_manager.store_secret(data['name'], data['value']) return jsonify({'stored': True}) @app.route('/api/vault/secrets/', methods=['GET']) def get_secret(name): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 value = app.vault_manager.get_secret(name) if value is None: return jsonify({'error': 'Not found'}), 404 return jsonify({'name': name, 'value': value}) @app.route('/api/vault/secrets/', methods=['DELETE']) def delete_secret(name): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 result = app.vault_manager.delete_secret(name) return jsonify({'deleted': result}) # Enhance container creation to support secrets @app.route('/api/containers', methods=['POST']) def create_container(): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 data = request.get_json(silent=True) if not data or 'image' not in data: return jsonify({'error': 'Missing image parameter'}), 400 name = data.get('name', '') env = data.get('env', {}) # If 'secrets' is provided, resolve secret values and add to env secrets = data.get('secrets', []) if secrets: for secret_name in secrets: secret_value = app.vault_manager.get_secret(secret_name) if secret_value is not None: env[secret_name] = secret_value volumes = data.get('volumes', {}) command = data.get('command', '') ports = data.get('ports', {}) result = container_manager.create_container( image=data['image'], name=name, env=env, volumes=volumes, command=command, ports=ports ) if 'error' in result: return jsonify(result), 500 return jsonify(result) @app.route('/api/containers/', methods=['DELETE']) def remove_container(name): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 force = request.args.get('force', default=False, type=bool) success = container_manager.remove_container(name, force=force) return jsonify({'removed': success}) @app.route('/api/images', methods=['GET']) def list_images(): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 images = container_manager.list_images() return jsonify(images) @app.route('/api/images/pull', methods=['POST']) def pull_image(): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 data = request.get_json(silent=True) if not data or 'image' not in data: return jsonify({'error': 'Missing image parameter'}), 400 result = container_manager.pull_image(data['image']) if 'error' in result: return jsonify(result), 500 return jsonify(result) @app.route('/api/images/', methods=['DELETE']) def remove_image(image): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 force = request.args.get('force', default=False, type=bool) success = container_manager.remove_image(image, force=force) return jsonify({'removed': success}) @app.route('/api/volumes', methods=['GET']) def list_volumes(): # Temporarily disable access control for debugging # if not is_local_request(): # return jsonify({'error': 'Access denied'}), 403 volumes = container_manager.list_volumes() return jsonify(volumes) @app.route('/api/volumes', methods=['POST']) def create_volume(): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 data = request.get_json(silent=True) if not data or 'name' not in data: return jsonify({'error': 'Missing name parameter'}), 400 result = container_manager.create_volume(data['name']) if 'error' in result: return jsonify(result), 500 return jsonify(result) @app.route('/api/volumes/', methods=['DELETE']) def remove_volume(name): if not is_local_request(): return jsonify({'error': 'Access denied'}), 403 force = request.args.get('force', default=False, type=bool) success = container_manager.remove_volume(name, force=force) return jsonify({'removed': success}) if __name__ == '__main__': app.run(host='0.0.0.0', port=3000, debug=True)