494 lines
18 KiB
Python
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 |