""" Blueprint para manejar las rutas de proxy a scripts Flask internos Rutas: /project/{project_id}/script/{script_id}/user/{user_id}/* """ import requests import logging import json import os from datetime import datetime from flask import Blueprint, request, Response, jsonify, current_app, abort from flask_login import login_required, current_user from werkzeug.exceptions import NotFound, ServiceUnavailable, Forbidden from typing import Optional import time from app.services.script_proxy_service import get_proxy_service from app.models import ScriptProxyExecution, UserProject, Script, User from app.config.database import db from app.config.permissions import can_access_script logger = logging.getLogger(__name__) # Crear blueprint proxy_bp = Blueprint('proxy', __name__, url_prefix='/project') @proxy_bp.route('//script//user//', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @proxy_bp.route('//script//user//', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @login_required def proxy_to_script(project_id: int, script_id: int, user_id: int, path: str = ''): """ Proxy principal que redirige todas las peticiones al script Flask interno """ logger.error(f"PROXY_TO_SCRIPT INICIO: project_id={project_id}, script_id={script_id}, user_id={user_id}, path={path}") try: # Validaciones de seguridad logger.error(f"PROXY_TO_SCRIPT: Llamando _validate_proxy_access") if not _validate_proxy_access(project_id, script_id, user_id): logger.error(f"PROXY_TO_SCRIPT: _validate_proxy_access retornó False") abort(403, "No tienes permisos para acceder a este script") # Obtener información del script en ejecución proxy_execution = _get_or_create_proxy_execution(project_id, script_id, user_id) if not proxy_execution: abort(503, "No se pudo iniciar el script") # Construir URL del script interno internal_url = f"http://localhost:{proxy_execution.internal_port}/{path}" # Actualizar actividad del script _update_script_activity(proxy_execution) # Proxy de la petición response = _proxy_request(internal_url, request) return response except requests.exceptions.ConnectionError: logger.error(f"Script en puerto {proxy_execution.internal_port if 'proxy_execution' in locals() else 'unknown'} no responde") abort(503, "El script no está disponible") except requests.exceptions.Timeout: logger.error(f"Timeout conectando al script en proyecto {project_id}") abort(504, "El script tardó demasiado en responder") except Exception as e: logger.error(f"Error en proxy para proyecto {project_id}, script {script_id}: {e}") abort(500, "Error interno del servidor") @proxy_bp.route('//script//user//_control/start', methods=['POST']) @login_required def start_script(project_id: int, script_id: int, user_id: int): """ Endpoint para iniciar explícitamente un script """ try: if not _validate_proxy_access(project_id, script_id, user_id): return jsonify({'error': 'No tienes permisos'}), 403 # Obtener información del script script = Script.query.get_or_404(script_id) project = UserProject.query.get_or_404(project_id) # Leer contenido del script script_content = _get_script_content(script) if not script_content: return jsonify({'error': 'No se pudo leer el contenido del script'}), 500 # Obtener parámetros de la petición data = request.get_json() or {} parameters = data.get('parameters', {}) environment = data.get('environment', {}) # Iniciar script a través del proxy service proxy_service = get_proxy_service() success, message, port = proxy_service.start_script( project_id=str(project_id), script_id=str(script_id), user_id=str(user_id), script_content=script_content, script_name=script.display_name or script.filename, parameters=parameters, environment=environment ) if success: # Crear o actualizar registro en base de datos proxy_execution = _create_proxy_execution_record( project_id, script_id, user_id, port, parameters, environment ) proxy_url = f"/project/{project_id}/script/{script_id}/user/{user_id}/" return jsonify({ 'success': True, 'message': message, 'proxy_url': proxy_url, 'internal_port': port, 'script_key': f"{project_id}_{script_id}_{user_id}" }), 200 else: return jsonify({ 'success': False, 'error': message }), 503 except Exception as e: logger.error(f"Error iniciando script: {e}") return jsonify({'error': 'Error interno del servidor'}), 500 @proxy_bp.route('//script//user//_control/stop', methods=['POST']) @login_required def stop_script(project_id: int, script_id: int, user_id: int): """ Endpoint para detener un script """ try: if not _validate_proxy_access(project_id, script_id, user_id): return jsonify({'error': 'No tienes permisos'}), 403 proxy_service = get_proxy_service() success, message = proxy_service.stop_script( str(project_id), str(script_id), str(user_id) ) if success: # Actualizar registro en base de datos proxy_execution = ScriptProxyExecution.query.filter_by( project_id=project_id, script_id=script_id, user_id=user_id ).first() if proxy_execution: proxy_execution.status = 'stopped' proxy_execution.stopped_at = datetime.utcnow() db.session.commit() return jsonify({ 'success': success, 'message': message }), 200 if success else 500 except Exception as e: logger.error(f"Error deteniendo script: {e}") return jsonify({'error': 'Error interno del servidor'}), 500 @proxy_bp.route('//script//user//_control/status', methods=['GET']) @login_required def script_status(project_id: int, script_id: int, user_id: int): """ Endpoint para obtener el estado de un script """ try: if not _validate_proxy_access(project_id, script_id, user_id): return jsonify({'error': 'No tienes permisos'}), 403 proxy_service = get_proxy_service() script_info = proxy_service.get_script_info( str(project_id), str(script_id), str(user_id) ) # Obtener también información de la base de datos proxy_execution = ScriptProxyExecution.query.filter_by( project_id=project_id, script_id=script_id, user_id=user_id ).first() status_data = { 'script_key': f"{project_id}_{script_id}_{user_id}", 'running': script_info is not None, 'proxy_service_info': script_info, 'database_info': proxy_execution.to_dict() if proxy_execution else None } return jsonify(status_data), 200 except Exception as e: logger.error(f"Error obteniendo estado del script: {e}") return jsonify({'error': 'Error interno del servidor'}), 500 @proxy_bp.route('/_admin/stats', methods=['GET']) @login_required def proxy_stats(): """ Endpoint administrativo para obtener estadísticas del proxy """ try: # Solo admins pueden ver estas estadísticas if current_user.user_level != 'admin': return jsonify({'error': 'Acceso denegado'}), 403 proxy_service = get_proxy_service() stats = proxy_service.get_proxy_stats() # Añadir estadísticas de base de datos db_stats = { 'total_executions': ScriptProxyExecution.query.count(), 'active_executions': ScriptProxyExecution.query.filter_by(status='running').count(), 'executions_by_status': {} } # Obtener conteos por estado from sqlalchemy import func status_counts = db.session.query( ScriptProxyExecution.status, func.count(ScriptProxyExecution.id) ).group_by(ScriptProxyExecution.status).all() for status, count in status_counts: db_stats['executions_by_status'][status] = count stats['database_stats'] = db_stats return jsonify(stats), 200 except Exception as e: logger.error(f"Error obteniendo estadísticas del proxy: {e}") return jsonify({'error': 'Error interno del servidor'}), 500 # === FUNCIONES AUXILIARES === def _validate_proxy_access(project_id: int, script_id: int, user_id: int) -> bool: """ Valida si el usuario actual puede acceder al script """ try: # SOLUCIÓN TEMPORAL: Si el usuario es admin, permitir acceso directo if current_user.user_level == 'admin': return True # Verificar que el proyecto existe y pertenece al usuario project = UserProject.query.filter_by( id=project_id, user_id=current_user.id ).first() if not project: logger.warning(f"Usuario {current_user.id} intentó acceder a proyecto {project_id} sin permisos") return False # Verificar que el script existe y está en el grupo del proyecto script = Script.query.filter_by( id=script_id, group_id=project.group_id ).first() if not script: logger.warning(f"Script {script_id} no encontrado en grupo {project.group_id}") return False # Verificar permisos de acceso al script can_access = can_access_script(current_user.user_level, script.required_level) if not can_access: logger.warning(f"Usuario {current_user.id} sin permisos para script {script_id}") return False # Si user_id es diferente al usuario actual, verificar permisos de admin if user_id != current_user.id and current_user.user_level != 'admin': logger.warning(f"Usuario {current_user.id} intentó acceder a script de usuario {user_id}") return False logger.error(f"DEBUG: Validación exitosa") return True except Exception as e: logger.error(f"Error validando acceso al proxy: {e}") return False def _get_or_create_proxy_execution(project_id: int, script_id: int, user_id: int) -> Optional[ScriptProxyExecution]: """ Obtiene una ejecución existente o crea una nueva """ try: # Buscar ejecución existente proxy_execution = ScriptProxyExecution.query.filter_by( project_id=project_id, script_id=script_id, user_id=user_id ).first() # Si existe y está activa, devolverla if proxy_execution and proxy_execution.status == 'running': proxy_service = get_proxy_service() script_info = proxy_service.get_script_info( str(project_id), str(script_id), str(user_id) ) if script_info: return proxy_execution else: # El script no está ejecutándose, limpiar registro proxy_execution.status = 'stopped' proxy_execution.stopped_at = datetime.utcnow() db.session.commit() # Intentar iniciar nuevo script script = Script.query.get(script_id) if not script: return None script_content = _get_script_content(script) if not script_content: return None proxy_service = get_proxy_service() success, message, port = proxy_service.start_script( project_id=str(project_id), script_id=str(script_id), user_id=str(user_id), script_content=script_content, script_name=script.display_name or script.filename ) if success: return _create_proxy_execution_record(project_id, script_id, user_id, port) return None except Exception as e: logger.error(f"Error obteniendo/creando ejecución proxy: {e}") return None def _create_proxy_execution_record(project_id: int, script_id: int, user_id: int, port: int, parameters: dict = None, environment: dict = None) -> ScriptProxyExecution: """ Crea un registro de ejecución proxy en la base de datos """ try: # Eliminar registro existente si lo hay existing = ScriptProxyExecution.query.filter_by( project_id=project_id, script_id=script_id, user_id=user_id ).first() if existing: db.session.delete(existing) # Crear nuevo registro proxy_execution = ScriptProxyExecution( project_id=project_id, script_id=script_id, user_id=user_id, internal_port=port, proxy_path=f"/project/{project_id}/script/{script_id}/user/{user_id}/", workspace_path=f"/app/workspaces/user_{user_id}/project_{project_id}/script_{script_id}", status='running', started_at=datetime.utcnow(), parameters=json.dumps(parameters) if parameters else None, environment_vars=json.dumps(environment) if environment else None ) db.session.add(proxy_execution) db.session.commit() return proxy_execution except Exception as e: logger.error(f"Error creando registro de ejecución proxy: {e}") db.session.rollback() raise def _get_script_content(script: Script) -> Optional[str]: """ Lee el contenido del archivo de script """ try: script_path = os.path.join( script.script_group.directory_path, script.filename ) if os.path.exists(script_path): with open(script_path, 'r', encoding='utf-8') as f: return f.read() logger.error(f"Archivo de script no encontrado: {script_path}") return None except Exception as e: logger.error(f"Error leyendo contenido del script: {e}") return None def _update_script_activity(proxy_execution: ScriptProxyExecution): """ Actualiza la actividad del script """ try: proxy_service = get_proxy_service() proxy_service.update_script_activity( str(proxy_execution.project_id), str(proxy_execution.script_id), str(proxy_execution.user_id) ) proxy_execution.update_activity() db.session.commit() except Exception as e: logger.error(f"Error actualizando actividad del script: {e}") def _proxy_request(target_url: str, original_request) -> Response: """ Proxy de la petición HTTP al script interno """ try: # Preparar headers (filtrar headers de hop-by-hop) headers = {} hop_by_hop = { 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'upgrade' } for key, value in original_request.headers: if key.lower() not in hop_by_hop: headers[key] = value # Agregar headers específicos del proxy headers['X-Forwarded-For'] = original_request.remote_addr headers['X-Forwarded-Proto'] = original_request.scheme headers['X-Forwarded-Host'] = original_request.host # Preparar datos de la petición request_kwargs = { 'method': original_request.method, 'url': target_url, 'headers': headers, 'params': original_request.args, 'allow_redirects': False, 'timeout': 30 } # Incluir cuerpo de la petición si es POST/PUT/PATCH if original_request.method in ['POST', 'PUT', 'PATCH']: if original_request.content_type and 'application/json' in original_request.content_type: request_kwargs['json'] = original_request.get_json() else: request_kwargs['data'] = original_request.get_data() # Realizar petición al script interno response = requests.request(**request_kwargs) # Preparar respuesta para el cliente excluded_headers = { 'content-encoding', 'content-length', 'transfer-encoding', 'connection' } response_headers = {} for key, value in response.headers.items(): if key.lower() not in excluded_headers: response_headers[key] = value # Crear respuesta Flask flask_response = Response( response.content, status=response.status_code, headers=response_headers, mimetype=response.headers.get('content-type', 'text/html') ) return flask_response except requests.exceptions.RequestException as e: logger.error(f"Error en petición proxy a {target_url}: {e}") raise except Exception as e: logger.error(f"Error general en proxy: {e}") raise