SIDEL_ScriptsManager/app/routes/proxy_routes.py

494 lines
18 KiB
Python

"""
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('/<int:project_id>/script/<int:script_id>/user/<int:user_id>/',
defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@proxy_bp.route('/<int:project_id>/script/<int:script_id>/user/<int:user_id>/<path:path>',
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('/<int:project_id>/script/<int:script_id>/user/<int:user_id>/_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('/<int:project_id>/script/<int:script_id>/user/<int:user_id>/_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('/<int:project_id>/script/<int:script_id>/user/<int:user_id>/_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