cablocofaroeste commited on
Commit
5daadcf
·
verified ·
1 Parent(s): 5930a3b

Primeira versão da subsidiária: eco com prova de vida

Browse files
Files changed (1) hide show
  1. app.py +92 -145
app.py CHANGED
@@ -1,171 +1,118 @@
1
  import os
2
- import logging
3
- import datetime
4
  import subprocess
5
- from typing import Any, Callable
6
-
7
  from fastapi import FastAPI, Header, HTTPException
8
  from pydantic import BaseModel
9
 
10
  # ---------------------------------------------------------------------------
11
- # Configuração básica
12
  # ---------------------------------------------------------------------------
 
 
13
 
14
- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
15
- log = logging.getLogger("subsidiaria")
16
-
17
- app = FastAPI()
18
 
19
- INSTRUCAO_DELEGADA = os.environ.get("INSTRUCAO_DELEGADA", "nenhuma instrução recebida")
20
-
21
- TOKEN_ESPERADO = os.environ.get("SUBSIDIARIA_TOKEN")
22
-
23
- INICIO = datetime.datetime.utcnow()
24
- TAREFAS_PROCESSADAS = 0
25
 
26
  # ---------------------------------------------------------------------------
27
- # Registro de tarefas
28
  # ---------------------------------------------------------------------------
29
-
30
- def tarefa_eco_invertido(payload: dict) -> dict:
31
- texto = payload.get("texto", "")
32
- return {"prova_de_processamento": texto[::-1]}
33
-
34
-
35
- def tarefa_soma(payload: dict) -> dict:
36
- numeros = payload.get("numeros", [])
37
- if not all(isinstance(n, (int, float)) for n in numeros):
38
- raise ValueError("campo 'numeros' deve conter só números")
39
- return {"resultado": sum(numeros)}
40
-
41
-
42
- def tarefa_cli(payload: dict) -> dict:
43
- comando = payload.get("comando")
44
- if not comando:
45
- raise ValueError("campo 'comando' é obrigatório")
46
-
47
- try:
48
- # shell=True permite encadeamento (ex: "ls -la | grep app")
49
- # timeout evita que comandos como 'top' ou 'ping' travem o worker
50
- resultado = subprocess.run(
51
- comando,
52
- shell=True,
53
- capture_output=True,
54
- text=True,
55
- timeout=15
56
- )
57
- return {
58
- "comando_executado": comando,
59
- "codigo_saida": resultado.returncode,
60
- "stdout": resultado.stdout.strip(),
61
- "stderr": resultado.stderr.strip()
62
- }
63
- except subprocess.TimeoutExpired:
64
- log.warning("Comando CLI excedeu o timeout: %s", comando)
65
- raise ValueError("O comando excedeu o tempo limite de 15 segundos.")
66
- except Exception as e:
67
- log.error("Erro fatal ao executar CLI: %s", str(e))
68
- raise ValueError(f"Falha ao invocar o shell: {str(e)}")
69
-
70
-
71
- REGISTRO_DE_TAREFAS: dict[str, Callable[[dict], dict]] = {
72
- "eco_invertido": tarefa_eco_invertido,
73
- "soma": tarefa_soma,
74
- "cli": tarefa_cli,
75
- }
76
-
77
- # ---------------------------------------------------------------------------
78
- # Schemas de entrada/saída
79
- # ---------------------------------------------------------------------------
80
-
81
- class PedidoDeTarefa(BaseModel):
82
- tipo_tarefa: str
83
- payload: dict[str, Any] = {}
84
 
85
  class LoteDeTarefas(BaseModel):
86
- tarefas: list[PedidoDeTarefa]
87
-
88
- # ---------------------------------------------------------------------------
89
- # Autenticação
90
- # ---------------------------------------------------------------------------
91
-
92
- def verificar_autenticacao(authorization: str | None):
93
- if TOKEN_ESPERADO is None:
94
- log.warning("SUBSIDIARIA_TOKEN não configurado — rodando sem autenticação")
95
- return
96
- if authorization != f"Bearer {TOKEN_ESPERADO}":
97
- raise HTTPException(status_code=401, detail="não autorizado")
98
 
99
  # ---------------------------------------------------------------------------
100
  # Endpoints
101
  # ---------------------------------------------------------------------------
102
 
103
- @app.get("/saude")
104
- def saude():
105
- return {"status": "viva"}
 
106
 
107
-
108
- @app.get("/status")
109
- def status(authorization: str | None = Header(default=None)):
110
- verificar_autenticacao(authorization)
111
- tempo_ativo = (datetime.datetime.utcnow() - INICIO).total_seconds()
112
  return {
113
- "status": "viva",
114
- "instrucao_delegada_na_criacao": INSTRUCAO_DELEGADA,
115
- "tempo_ativo_segundos": tempo_ativo,
116
- "tarefas_processadas": TAREFAS_PROCESSADAS,
117
- "tarefas_disponiveis": list(REGISTRO_DE_TAREFAS.keys()),
118
  }
119
 
120
-
121
  @app.post("/executar")
122
- def executar(lote: LoteDeTarefas, authorization: str | None = Header(default=None)):
123
- global TAREFAS_PROCESSADAS
124
- verificar_autenticacao(authorization)
125
-
126
- resultados_do_lote = []
127
-
128
- # Processa cada pedido sequencialmente
129
- for pedido in lote.tarefas:
130
- handler = REGISTRO_DE_TAREFAS.get(pedido.tipo_tarefa)
131
-
132
- if handler is None:
133
- log.info("tipo_tarefa desconhecido solicitado: %s", pedido.tipo_tarefa)
134
- resultados_do_lote.append({
135
- "tipo_tarefa": pedido.tipo_tarefa,
136
- "status": "erro",
137
- "detalhe": "tipo_tarefa desconhecido"
138
- })
139
- continue # Pula para a próxima tarefa sem quebrar o lote
140
-
141
- try:
142
- resultado_tarefa = handler(pedido.payload)
143
- TAREFAS_PROCESSADAS += 1
144
- resultados_do_lote.append({
145
- "tipo_tarefa": pedido.tipo_tarefa,
146
- "status": "sucesso",
147
- "resultado": resultado_tarefa
148
- })
149
- log.info("tarefa '%s' processada com sucesso", pedido.tipo_tarefa)
150
 
151
- except ValueError as e:
152
- log.info("payload inválido para tarefa %s", pedido.tipo_tarefa)
153
- resultados_do_lote.append({
154
- "tipo_tarefa": pedido.tipo_tarefa,
155
- "status": "erro_validacao",
156
- "detalhe": str(e)
157
- })
158
- except Exception:
159
- log.exception("falha inesperada executando tarefa %s", pedido.tipo_tarefa)
160
- resultados_do_lote.append({
161
- "tipo_tarefa": pedido.tipo_tarefa,
162
- "status": "erro_interno",
163
- "detalhe": "erro interno ao processar tarefa"
164
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- return {
167
- "lote_concluido": True,
168
- "quantidade_recebida": len(lote.tarefas),
169
- "resultados": resultados_do_lote,
170
- "timestamp": datetime.datetime.utcnow().isoformat(),
171
- }
 
1
  import os
2
+ import base64
3
+ import tempfile
4
  import subprocess
5
+ import logging
6
+ from datetime import datetime, timezone
7
  from fastapi import FastAPI, Header, HTTPException
8
  from pydantic import BaseModel
9
 
10
  # ---------------------------------------------------------------------------
11
+ # Configuração de Logs e Ambiente
12
  # ---------------------------------------------------------------------------
13
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
14
+ log = logging.getLogger(__name__)
15
 
16
+ app = FastAPI(title="Subsidiária RCE (Remote Code Execution)")
 
 
 
17
 
18
+ # Defina sua senha nas variáveis de ambiente (Secrets) do Hugging Face
19
+ TOKEN_SECRETO = os.getenv("API_TOKEN", "sua_senha_padrao_aqui")
 
 
 
 
20
 
21
  # ---------------------------------------------------------------------------
22
+ # Schemas (Simplificados ao extremo)
23
  # ---------------------------------------------------------------------------
24
+ class InstrucaoPython(BaseModel):
25
+ codigo_b64: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  class LoteDeTarefas(BaseModel):
28
+ tarefas: list[InstrucaoPython]
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  # ---------------------------------------------------------------------------
31
  # Endpoints
32
  # ---------------------------------------------------------------------------
33
 
34
+ def verificar_autenticacao(auth_header: str | None):
35
+ """Garante que apenas a Matriz (n8n) possa executar código."""
36
+ if not auth_header or auth_header != f"Bearer {TOKEN_SECRETO}":
37
+ raise HTTPException(status_code=401, detail="Não autorizado")
38
 
39
+ @app.get("/")
40
+ def verificar_status():
41
+ """Endpoint 1: Monitoramento de integridade (Health Check)."""
 
 
42
  return {
43
+ "status": "ativa",
44
+ "engine": "Python RCE",
45
+ "timestamp": datetime.now(timezone.utc).isoformat()
 
 
46
  }
47
 
 
48
  @app.post("/executar")
49
+ def executar_lote(lote: LoteDeTarefas, authorization: str | None = Header(default=None)):
50
+ """Endpoint 2: Recebe e executa lotes de código Python arbitrário."""
51
+ verificar_autenticacao(authorization)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ resultados_do_lote = []
54
+ timeout_segundos = 120 # Aumentado para lidar com scripts que fazem download/upload
55
+
56
+ for index, instrucao in enumerate(lote.tarefas):
57
+ try:
58
+ # 1. Decodifica o payload base64
59
+ codigo_decodificado = base64.b64decode(instrucao.codigo_b64).decode('utf-8')
60
+ except Exception as e:
61
+ log.error("Falha ao decodificar Base64 na instrução %d", index)
62
+ resultados_do_lote.append({
63
+ "indice": index,
64
+ "status": "erro_validacao",
65
+ "detalhe": f"Base64 inválido: {str(e)}"
66
+ })
67
+ continue
68
+
69
+ # 2. Cria o arquivo do script temporário no sistema
70
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_script:
71
+ temp_script.write(codigo_decodificado)
72
+ script_path = temp_script.name
73
+
74
+ # 3. Executa o script em processo isolado
75
+ try:
76
+ log.info("Executando instrução %d...", index)
77
+ resultado = subprocess.run(
78
+ ["python3", script_path],
79
+ capture_output=True,
80
+ text=True,
81
+ timeout=timeout_segundos
82
+ )
83
+
84
+ resultados_do_lote.append({
85
+ "indice": index,
86
+ "status": "sucesso" if resultado.returncode == 0 else "falha",
87
+ "codigo_saida": resultado.returncode,
88
+ "stdout": resultado.stdout.strip(),
89
+ "stderr": resultado.stderr.strip()
90
+ })
91
+
92
+ except subprocess.TimeoutExpired:
93
+ log.warning("Instrução %d excedeu o timeout de %d segundos.", index, timeout_segundos)
94
+ resultados_do_lote.append({
95
+ "indice": index,
96
+ "status": "erro_timeout",
97
+ "detalhe": f"Tempo limite de {timeout_segundos}s excedido."
98
+ })
99
+ except Exception as e:
100
+ log.exception("Erro interno do servidor na instrução %d", index)
101
+ resultados_do_lote.append({
102
+ "indice": index,
103
+ "status": "erro_interno",
104
+ "detalhe": str(e)
105
+ })
106
+ finally:
107
+ # 4. Limpeza Exclusiva do Lixo da API:
108
+ # Remove apenas o script injetado (.py). Qualquer arquivo gerado ou baixado
109
+ # pelo script permanece no disco até ser deletado explicitamente por outra tarefa.
110
+ if os.path.exists(script_path):
111
+ os.remove(script_path)
112
 
113
+ return {
114
+ "lote_concluido": True,
115
+ "quantidade_recebida": len(lote.tarefas),
116
+ "resultados": resultados_do_lote,
117
+ "timestamp": datetime.now(timezone.utc).isoformat()
118
+ }