Skip to content

feat/37 - #82

Merged
MariaEduardaBatt merged 40 commits into
mainfrom
feat/37
Jun 17, 2026
Merged

MariaEduardaBatt merged 40 commits into
mainfrom
feat/37

Conversation

@MariaEduardaBatt

@MariaEduardaBatt MariaEduardaBatt commented Jun 17, 2026 •

Copy link
Copy Markdown
Owner

Closes #37

O que mudou

Adiciona rate limit de 30 execuções/min por usuário e 120/min por IP usando flask-limiter com backend in-memory (memory://).

Alterações

  • rate_limiter.py: SlidingWindowRateLimiter — sliding window in-memory (mantido como módulo standalone)
  • routes/limits.py: GET /api/limits — endpoint público com limites atuais
  • routes/compile.py: @limiter.limit("{runs_per_minute}/minute", key_func=_user_key) para rate por user_id + IP
  • routes/run_ws.py: limiter.limit(...) como context manager no handler WebSocket
  • app.py: Limiter inicializado com memory://, injetado nos blueprints e WebSocket
  • config.py: novos campos runs_per_minute (30) e runs_per_minute_ip (120)
  • requirements.txt: adicionado flask-limiter>=3.0.0
  • docker-compose.yml: variáveis RUNS_PER_MINUTE e RUNS_PER_MINUTE_IP no backend

Testes

  • test_rate_limiter.py: 10 testes (unidade do custom limiter + integração com flask-limiter)
  • 66/67 testes passando (falha pré-existente em test_config.py)

Limitação conhecida

O backend usa 4 workers do gunicorn (-w 4). O backend in-memory é por worker, então o limite efetivo é 30 × 4 = 120 execuções/min. Para um limite exato entre workers, seria necessário Redis (descartado por enquanto devido a problemas de conexão).

Como testar

  1. docker compose down && docker compose up -d --build
  2. Fazer 31 requisições rápidas para /api/compile
  3. A 31a deve retornar 429 rate_limit_exceeded

LucaS4nt0s and others added 30 commits June 13, 2026 13:01
…ile-timeout

feat(backend): enforce 15s compile timeout (#25)
feat(frontend): replace terminal panel with xterm.js interactive terminal (#27)
…rategy

feat(backend): implement PtyExecutionStrategy (#29)
…-pty

feat(backend): bridge websocket and pty streams (#30)
Fixes for issue #30:
- Change gunicorn worker from GeventWebSocketWorker to gevent
  (flask-sock's simple-websocket was incompatible with gevent-websocket)
- Use container.create() + put_archive() instead of bind mount
  (bind mount fails between sibling containers)
- Start reader thread before container.start() to avoid race
  condition (fast programs finish before reader starts)
- Remove read_only=True to allow put_archive to write binary
- Remove tmpfs on /sandbox (tmpfs hid the uploaded binary)
The frontend sends the Supabase JWT token via WebSocket subprotocol
(bearer.<token>) which is transmitted in the Sec-WebSocket-Protocol
header. Without this header being forwarded, the backend cannot
authenticate the WebSocket connection and closes it with
'missing_bearer_token'.
Supabase access_tokens use ES256 (ECDSA) algorithm, not HS256.
- Add ES256 to allowed algorithms in verify_supabase_jwt()
- Add cryptography dependency required for ES256 support
Supabase access_tokens use ES256 (ECDSA) which requires a public
key, not a symmetric secret. This fix:
- Detects the token algorithm (ES256 vs HS256)
- For ES256: fetches the public key from Supabase JWKS endpoint
  and verifies with it
- For HS256: verifies with the JWT secret as before
- Caches the JWKS client to avoid repeated fetches
Browsers expect the server to respond with a Sec-WebSocket-Protocol
header when the client sends one. simple-websocket's default
choose_subprotocol returns None when no subprotocols are configured,
causing the browser to fire onerror.

Fix: monkey-patch choose_subprotocol to accept the first subprotocol
the client sends, so the 101 response includes the expected header.
- Replace PyJWKClient with manual JWKS fetch + EC key construction
- Fallback to HS256 if ES256+JWKS fails
- Log JWT alg, kid, and any verification errors
- Add logging.basicConfig in create_app
feat(backend): bridge websocket and pty streams
The PtyExecutionStrategy main loop was breaking on every
ws.receive(timeout=0.05) that returned None (normal timeout),
which killed the container before any stdin could be forwarded.

This prevented the interactive leia flow from working — the
program would block on sys_read, but the backend loop would
exit within 50ms, never receiving the user's input.

Changing break to continue keeps the loop alive, polling stdout,
wall-clock timeout, and incoming WebSocket messages (stdin,
stop, ping) as intended.
fix(backend): keep execution loop alive on ws.receive timeout
feat(backend): implement websocket protocol events (#32)
feat(frontend): wire stop action to websocket
- exit event skipped when timed_out=True
- container uses read_only=True with binary on tmpfs
- container.stop() for graceful shutdown
- frontend preserves error state on ws close
- tests for timeout flow
gevent-websocket conflicts with the subprotocol monkey-patch
used for JWT auth. flask-sock falls back to simple-websocket
which correctly handles bearer.<token> subprotocol.
feat(backend): enforce 10s execution timeout (#34)
…-timeout

feat(devops): set docker hard stop timeout to 12s
- Set read_only=True and replace put_archive with bind mount ro
- Add tmpfs at /tmp for writable temp storage
- Remove unused io, os, tarfile imports
… workers

Substitui o SlidingWindowRateLimiter in-memory (per-worker) pelo
flask-limiter com Redis, garantindo que o limite de 30 runs/min
seja compartilhado entre todos os workers do gunicorn.

Adiciona serviço redis ao docker-compose.yml e injeta o limiter
no blueprint REST e no handler WebSocket.
Adiciona try/except na conexao Redis na inicializacao. Se o Redis
nao estiver pronto, faz fallback para memory:// com warning.
Adiciona healthcheck no Redis no docker-compose.
Bind mount do tmpdir do Windows falha no Docker Desktop (caminho
C:\Users\... nao monta como /sandbox). Agora copia o binario
via put_archive() para /tmp dentro do container.
Em vez de put_archive/bind mount, o binario agora e codificado
em base64 e passado via comando sh -c. O container decodifica
e executa qemu com o binario estatico em /tmp/programa.
@MariaEduardaBatt MariaEduardaBatt linked an issue Jun 17, 2026 that may be closed by this pull request
4 tasks
@MariaEduardaBatt
MariaEduardaBatt merged commit 0deb373 into main Jun 17, 2026
@MariaEduardaBatt
MariaEduardaBatt deleted the feat/37 branch June 17, 2026 01:48
@MariaEduardaBatt
MariaEduardaBatt restored the feat/37 branch June 17, 2026 01:54
@MariaEduardaBatt
MariaEduardaBatt deleted the feat/37 branch June 17, 2026 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(backend): add per-user execution rate limit

2 participants