diff --git a/README.md b/README.md index 90fc520..363646e 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,148 @@ # thePlug -Wrapper web per gestire proxy `mitmdump` davanti a servizi esposti via Docker +Web UI per gestire proxy `mitmdump` davanti a servizi esposti via Docker Compose. ## Struttura ```text -backend/ FastAPI, MongoDB, mitmdump manager -frontend/ Vue 3, Vite, CodeMirror + Python LSP -playground/ servizio locale minimale per test end-to-end +backend/ FastAPI, MongoDB, mitmdump manager +frontend/ Vue 3, Vite, CodeMirror + Python LSP +playground/ challenge/service compose locali +templates/ template proxy YAML file-backed ``` -## Setup rapido +## Setup iniziale ```bash ./setup.sh ``` -Lo script crea `backend/.env`, genera le credenziali mancanti e avvia lo stack -Docker completo: +`setup.sh` fa bootstrap della macchina/progetto: +- installa `uv` se manca +- installa Python `3.14` via `uv` +- crea `backend/.env` se manca +- installa dipendenze backend e frontend +- builda il frontend +- prepara MongoDB e l'immagine Docker del frontend -```text -Frontend http://localhost:5173 -Backend http://localhost:3671 +## Runtime reale / vulnbox + +Questi sono i comandi pensati per l’ambiente reale di gara. + +### MongoDB + +```bash +./run.sh mongo up +./run.sh mongo status +./run.sh mongo logs +./run.sh mongo down ``` -Questa modalità è buona per smoke test UI/API. Se vuoi provare davvero il -takeover della porta di un servizio Docker locale, fai girare il backend sulla +### Backend + +```bash +./run.sh backend +``` + +### Frontend Docker + +```bash +./run.sh frontend up +./run.sh frontend status +./run.sh frontend logs +./run.sh frontend down +``` + +Il frontend in container fa proxy API verso il backend in esecuzione sulla macchina host. -Guida completa: [TESTING.md](/home/gabdevele/CTF/thePlug/TESTING.md) +## Workflow development -Per installare anche il servizio systemd user: +Questi comandi sono pensati per sviluppo locale, test e playground. + +### MongoDB ```bash -./setup.sh service +./dev.sh mongo up +./dev.sh mongo status +./dev.sh mongo logs +./dev.sh mongo down ``` -## Dev backend +### Backend dev ```bash -cp backend/.env.example backend/.env -docker compose --env-file backend/.env up -d mongodb -npm run dev:backend +./dev.sh backend ``` Docs FastAPI in dev: `http://localhost:3671/docs`. -## Test locale realistico - -Per simulare il deployment reale, usa il backend sulla macchina host: +### Frontend dev ```bash -./setup.sh deps -export SERVICE_SCAN_ROOT="$PWD" -npm run dev:backend -npm run dev:frontend -./setup.sh playground +./dev.sh frontend ``` -Poi: +Questo avvia Vite normalmente sulla macchina host. -1. Apri `http://localhost:5173`. -2. Fai login con le credenziali in `backend/.env`. -3. Crea un nuovo proxy. -4. Seleziona il servizio `echo` rilevato dal backend sotto la cartella `playground`. -5. Lascia `mode=reverse`. -6. Verifica che `proxy_port` combaci con la porta del servizio (`9081`) se vuoi che il proxy prenda davvero il posto della route originale. -7. Salva e premi `Start`. -8. Esegui `curl http://127.0.0.1:9081/`. +### Playground / servizi locali -Se il proxy è attivo, la route originale è ora posseduta da `mitmdump` e il -servizio reale è stato spostato su una porta upstream temporanea, visibile nella -UI e nello stato runtime. - -`SERVICE_SCAN_ROOT` controls where the backend looks for services. The scan is -intentionally shallow: it inspects only the immediate child directories of that -root, which matches the expected host layout. - -## Dev frontend +Per vedere i challenge/service disponibili: ```bash -cd frontend -npm install -npm run dev +./dev.sh services list ``` -Il dev server usa `/api` come proxy verso `http://127.0.0.1:3671`. +Per avviarli o fermarli: + +```bash +./dev.sh services up GabibbiTowers +./dev.sh services down GabibbiTowers + +./dev.sh services up all +./dev.sh services status all +./dev.sh services down all +``` + +`dev.sh` scopre automaticamente i compose da `playground/*/docker-compose.yml`. + +## Flussi consigliati + +Runtime reale: + +```bash +./setup.sh +./run.sh mongo up +./run.sh backend +./run.sh frontend up +``` + +Sviluppo locale: + +```bash +./setup.sh +./dev.sh mongo up +./dev.sh backend +./dev.sh frontend +./dev.sh services up +``` ## Test +Backend: + ```bash -npm run test +cd backend +uv run ruff check . +uv run pytest ``` -Equivalente a: +Frontend: ```bash -cd backend && uv run ruff check . && uv run pytest -cd frontend && npm run build -``` - -Per testare solo il playground: - -```bash -npm run playground:up -npm run playground:down +cd frontend +npm run build ``` ## API principali @@ -121,7 +158,7 @@ npm run playground:down - `GET /proxy/{port}/logs` - `WS /proxy/{port}/logs/ws` - `GET /proxy/mitmproxy/options` -- `GET /proxy/mitmproxy/examples` +- `GET /proxy/mitmproxy/templates` ## Riferimenti diff --git a/TODO b/TODO index 4711bf2..8126c86 100644 --- a/TODO +++ b/TODO @@ -1,7 +1 @@ -- con tante richieste contemporaneamente la pagina inizia a laggare tantissimo per colpa dei log, quindi bisogna capire -come ottimizzare. -- rimuovere package.json -- sistemare setup.sh per poter avviare il server in modo più semplice -- creare un altro script bash per runnare i vari servizi in maniera semplice. -- aggiornare readme -- fixare i test di unità, di integrazione e aggiungere test reali di benchmark. \ No newline at end of file +- fixare i test di unità, di integrazione e aggiungere test reali di benchmark. diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 3998d9d..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM python:3.14-slim - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PYTHONPATH=/app/src - -WORKDIR /app - -COPY pyproject.toml ./ -RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir \ - "authlib>=1.6.0" \ - "basedpyright>=1.39.0" \ - "fastapi[standard]>=0.135.3" \ - "mitmproxy>=12.2.2" \ - "motor>=3.7.1" \ - "pwdlib[argon2,bcrypt]>=0.3.0" \ - "pyyaml>=6.0.3" \ - "pydantic-settings>=2.13.1" \ - "ruff>=0.15.10" - -COPY app ./src - -EXPOSE 3671 - -CMD ["fastapi", "run", "src/main.py", "--host", "0.0.0.0", "--port", "3671"] diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..7c74dd3 --- /dev/null +++ b/dev.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[+]${NC} $*"; } +die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; } + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +FRONTEND_DIR="$ROOT_DIR/frontend" +PLAYGROUND_DIR="$ROOT_DIR/playground" +ENV_FILE="$BACKEND_DIR/.env" + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +require_env() { + [[ -f "$ENV_FILE" ]] || die "Missing $ENV_FILE. Run ./setup.sh first." +} + +env_value() { + local key="$1" + local default_value="${2:-}" + local value + value=$(grep -E "^${key}=" "$ENV_FILE" | cut -d '=' -f2- || true) + if [[ -n "$value" ]]; then + printf '%s\n' "$value" + return + fi + printf '%s\n' "$default_value" +} + +compose() { + docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@" +} + +service_compose_path() { + local name="$1" + local path="$PLAYGROUND_DIR/$name/docker-compose.yml" + [[ -f "$path" ]] || die "Unknown service group: $name" + printf '%s\n' "$path" +} + +service_names() { + find "$PLAYGROUND_DIR" -mindepth 2 -maxdepth 2 -name docker-compose.yml -printf '%h\n' \ + | xargs -r -n1 basename \ + | sort +} + +run_backend() { + require_command uv + require_env + local api_port + api_port=$(env_value API_PORT 3671) + info "Starting backend dev server on http://0.0.0.0:$api_port" + ( + cd "$BACKEND_DIR" + uv run fastapi dev app/main.py --host 0.0.0.0 --port "$api_port" + ) +} + +run_frontend() { + require_command npm + require_env + info "Starting frontend Vite dev server" + ( + cd "$FRONTEND_DIR" + npm run dev + ) +} + +run_mongo() { + require_command docker + require_env + local action="${1:-status}" + case "$action" in + up) + compose up -d mongodb + ;; + down) + compose stop mongodb + ;; + status) + compose ps mongodb + ;; + logs) + compose logs -f mongodb + ;; + *) + die "Unknown mongo action: $action" + ;; + esac +} + +run_services() { + require_command docker + local action="${1:-list}" + shift || true + + case "$action" in + list) + service_names + ;; + up|down|status) + local target="${1:-all}" + if [[ "$target" == "all" ]]; then + while IFS= read -r name; do + [[ -n "$name" ]] || continue + local compose_file + compose_file=$(service_compose_path "$name") + case "$action" in + up) docker compose -f "$compose_file" up -d ;; + down) docker compose -f "$compose_file" down ;; + status) docker compose -f "$compose_file" ps ;; + esac + done < <(service_names) + return + fi + + local compose_file + compose_file=$(service_compose_path "$target") + case "$action" in + up) docker compose -f "$compose_file" up -d ;; + down) docker compose -f "$compose_file" down ;; + status) docker compose -f "$compose_file" ps ;; + esac + ;; + *) + die "Unknown services action: $action" + ;; + esac +} + +show_help() { + cat <<'EOF' +Usage: + ./dev.sh backend + ./dev.sh frontend + ./dev.sh mongo [up|down|status|logs] + ./dev.sh services [list|up|down|status] [name|all] + +Examples: + ./dev.sh mongo up + ./dev.sh backend + ./dev.sh frontend + ./dev.sh services list + ./dev.sh services up GabibbiTowers +EOF +} + +main() { + local command="${1:-help}" + shift || true + + case "$command" in + backend) + run_backend "$@" + ;; + frontend) + run_frontend "$@" + ;; + mongo) + run_mongo "$@" + ;; + services) + run_services "$@" + ;; + help|-h|--help) + show_help + ;; + *) + die "Unknown command: $command" + ;; + esac +} + +main "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 68fdd08..b83ef50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,12 @@ services: context: ./frontend container_name: theplug-frontend restart: unless-stopped + environment: + API_UPSTREAM: "host.docker.internal:${API_PORT:-3671}" ports: - "${FRONTEND_PORT:-5173}:80" - depends_on: - - backend + extra_hosts: + - "host.docker.internal:host-gateway" mongodb: image: mongo:8.2 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a80161b..ec3d735 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -10,7 +10,7 @@ RUN npm run build FROM nginx:1.29-alpine -COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/templates/default.conf.template COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 6f312b2..3ee9f00 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -6,7 +6,7 @@ server { index index.html; location /api/ { - proxy_pass http://backend:3671/; + proxy_pass http://${API_UPSTREAM}/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; diff --git a/frontend/src/composables/controlPlaneLogs.ts b/frontend/src/composables/controlPlaneLogs.ts index 8f01811..b8f2125 100644 --- a/frontend/src/composables/controlPlaneLogs.ts +++ b/frontend/src/composables/controlPlaneLogs.ts @@ -1,4 +1,4 @@ -import { computed, ref, watch, type Ref } from "vue"; +import { computed, ref, shallowRef, watch, type Ref } from "vue"; import { api, buildSocketUrl } from "../api"; @@ -11,6 +11,31 @@ type LogPlaneOptions = { onError: (error: unknown) => void; }; +const LOG_RECONNECT_DELAY_MS = 1200; +const LOG_FLUSH_DELAY_MS = 75; +const MAX_LOG_LINES = 2500; +const MAX_LOG_CHARS = 250_000; + +function splitLogText(text: string) { + return text === "" ? [] : text.split("\n"); +} + +function trimLogLines(lines: string[]) { + let next = lines.length > MAX_LOG_LINES ? lines.slice(-MAX_LOG_LINES) : lines; + let totalChars = next.reduce( + (count, line, index) => count + line.length + (index > 0 ? 1 : 0), + 0, + ); + + while (totalChars > MAX_LOG_CHARS && next.length > 1) { + const [removed = ""] = next; + next = next.slice(1); + totalChars -= removed.length + 1; + } + + return next; +} + export function useControlPlaneLogs({ token, selectedPort, @@ -21,20 +46,63 @@ export function useControlPlaneLogs({ const logFilter = ref(""); const logsPaused = ref(false); const logsCleared = ref(false); + const logLines = shallowRef([]); let logSocket: WebSocket | null = null; let logReconnectTimer: number | null = null; + let logFlushTimer: number | null = null; + let pendingLogChunk = ""; const filteredLogs = computed(() => { - const source = logs.value || ""; const query = logFilter.value.trim().toLowerCase(); - if (!query) return source; - return source - .split("\n") + if (!query) return logs.value; + return logLines.value .filter((line) => line.toLowerCase().includes(query)) .join("\n"); }); + function commitLogLines(lines: string[]) { + const trimmed = trimLogLines(lines); + logLines.value = trimmed; + logs.value = trimmed.join("\n"); + } + + function replaceLogs(text: string) { + commitLogLines(splitLogText(text)); + } + + function appendLogs(text: string) { + if (!text) return; + commitLogLines([...logLines.value, ...splitLogText(text)]); + } + + function flushPendingLogs() { + if (!pendingLogChunk) return; + appendLogs(pendingLogChunk); + pendingLogChunk = ""; + } + + function stopPendingFlush() { + if (typeof window === "undefined" || logFlushTimer === null) return; + window.clearTimeout(logFlushTimer); + logFlushTimer = null; + } + + function scheduleLogFlush() { + if (typeof window === "undefined" || logFlushTimer !== null) return; + logFlushTimer = window.setTimeout(() => { + logFlushTimer = null; + flushPendingLogs(); + }, LOG_FLUSH_DELAY_MS); + } + + function resetLogBuffer() { + stopPendingFlush(); + pendingLogChunk = ""; + logLines.value = []; + logs.value = ""; + } + function parseLogFrame(raw: string): LogFrame | null { try { const payload = JSON.parse(raw); @@ -55,7 +123,7 @@ export function useControlPlaneLogs({ if (port === null) return; try { if (!logsCleared.value) { - logs.value = (await api.logs(port)).log; + replaceLogs((await api.logs(port)).log); } } catch (error) { if (!silent) onError(error); @@ -72,7 +140,7 @@ export function useControlPlaneLogs({ logReconnectTimer = null; if (!token.value || logsPaused.value || selectedPort.value !== proxyPort) return; void openLogStream(proxyPort); - }, 1200); + }, LOG_RECONNECT_DELAY_MS); } async function openLogStream(proxyPort: number) { @@ -98,10 +166,13 @@ export function useControlPlaneLogs({ if (frame === null) return; if (frame.type === "snapshot") { if (logsCleared.value) return; - logs.value = frame.log; + resetLogBuffer(); + replaceLogs(frame.log); return; } - logs.value += frame.log; + + pendingLogChunk += frame.log; + scheduleLogFlush(); }; socket.onclose = (event) => { @@ -131,6 +202,8 @@ export function useControlPlaneLogs({ window.clearTimeout(logReconnectTimer); logReconnectTimer = null; } + stopPendingFlush(); + pendingLogChunk = ""; if (logSocket === null) return; logSocket.onclose = null; logSocket.close(); @@ -142,13 +215,13 @@ export function useControlPlaneLogs({ } function clearLogs() { - logs.value = ""; + resetLogBuffer(); logsCleared.value = true; } function resetLogs() { stopLogStream(); - logs.value = ""; + resetLogBuffer(); logFilter.value = ""; logsPaused.value = false; logsCleared.value = false; @@ -158,7 +231,7 @@ export function useControlPlaneLogs({ [selectedPort, token, logsPaused], ([port, authToken, paused], [previousPort]) => { if (port !== previousPort) { - logs.value = ""; + resetLogBuffer(); logFilter.value = ""; logsCleared.value = false; } diff --git a/package.json b/package.json deleted file mode 100644 index 25e8121..0000000 --- a/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "theplug", - "private": true, - "version": "0.1.0", - "scripts": { - "dev:backend": "cd backend && PYTHONPATH=. uv run fastapi dev app/main.py --host 0.0.0.0 --port 3671", - "dev:frontend": "cd frontend && npm run dev", - "dev:deps": "./setup.sh deps", - "test:backend": "cd backend && uv run pytest", - "lint:backend": "cd backend && uv run ruff check .", - "format:backend": "cd backend && uv run ruff format .", - "build:frontend": "cd frontend && npm run build", - "test": "npm run lint:backend && npm run test:backend && npm run build:frontend", - "compose:up": "./setup.sh", - "compose:down": "docker compose --env-file backend/.env -f docker-compose.yml down", - "playground:up": "docker compose -f playground/docker-compose.yml up -d", - "playground:down": "docker compose -f playground/docker-compose.yml down" - } -} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..a896fa7 --- /dev/null +++ b/run.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[+]${NC} $*"; } +die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; } + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +ENV_FILE="$BACKEND_DIR/.env" + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +require_env() { + [[ -f "$ENV_FILE" ]] || die "Missing $ENV_FILE. Run ./setup.sh first." +} + +env_value() { + local key="$1" + local default_value="${2:-}" + local value + value=$(grep -E "^${key}=" "$ENV_FILE" | cut -d '=' -f2- || true) + if [[ -n "$value" ]]; then + printf '%s\n' "$value" + return + fi + printf '%s\n' "$default_value" +} + +compose() { + docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@" +} + +run_backend() { + require_command uv + require_env + local api_port + api_port=$(env_value API_PORT 3671) + info "Starting backend on http://0.0.0.0:$api_port" + ( + cd "$BACKEND_DIR" + uv run fastapi run app/main.py --host 0.0.0.0 --port "$api_port" + ) +} + +run_frontend() { + require_command docker + require_env + local action="${1:-up}" + case "$action" in + up) + compose up -d frontend + info "Frontend container running on http://localhost:$(env_value FRONTEND_PORT 5173)" + ;; + down) + compose stop frontend + ;; + status) + compose ps frontend + ;; + logs) + compose logs -f frontend + ;; + *) + die "Unknown frontend action: $action" + ;; + esac +} + +run_mongo() { + require_command docker + require_env + local action="${1:-status}" + case "$action" in + up) + compose up -d mongodb + info "MongoDB running on mongodb://localhost:27017" + ;; + down) + compose stop mongodb + ;; + status) + compose ps mongodb + ;; + logs) + compose logs -f mongodb + ;; + *) + die "Unknown mongo action: $action" + ;; + esac +} + +show_help() { + cat <<'EOF' +Usage: + ./run.sh backend + ./run.sh frontend [up|down|status|logs] + ./run.sh mongo [up|down|status|logs] + +Examples: + ./run.sh mongo up + ./run.sh backend + ./run.sh frontend up + ./run.sh frontend logs +EOF +} + +main() { + local command="${1:-help}" + shift || true + + case "$command" in + backend) + run_backend "$@" + ;; + frontend) + run_frontend "$@" + ;; + mongo) + run_mongo "$@" + ;; + help|-h|--help) + show_help + ;; + *) + die "Unknown command: $command" + ;; + esac +} + +main "$@" diff --git a/setup.sh b/setup.sh index d3047ed..4709112 100755 --- a/setup.sh +++ b/setup.sh @@ -11,14 +11,29 @@ warn() { echo -e "${YELLOW}[!]${NC} $*"; } die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; } ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ENV_FILE="$ROOT_DIR/backend/.env" +BACKEND_DIR="$ROOT_DIR/backend" +FRONTEND_DIR="$ROOT_DIR/frontend" +ENV_FILE="$BACKEND_DIR/.env" -compose() { - docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@" +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" } -playground() { - docker compose -f "$ROOT_DIR/playground/docker-compose.yml" "$@" +ensure_uv() { + if command -v uv >/dev/null 2>&1; then + return + fi + + info "Installing uv..." + require_command curl + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + command -v uv >/dev/null 2>&1 || die "uv installed but not found in PATH" +} + +ensure_python() { + info "Ensuring Python 3.14 is available via uv..." + uv python install 3.14 } sync_env_values() { @@ -35,11 +50,12 @@ sync_env_values() { ensure_env() { if [[ -f "$ENV_FILE" ]]; then + sync_env_values return fi warn "backend/.env not found, creating it from backend/.env.example" - cp "$ROOT_DIR/backend/.env.example" "$ENV_FILE" + cp "$BACKEND_DIR/.env.example" "$ENV_FILE" local mongo_pass jwt_secret auth_pass mongo_pass=$(openssl rand -hex 8) @@ -56,74 +72,58 @@ ensure_env() { info "AUTH_PASSWORD: $auth_pass" } -install_service() { - mkdir -p ~/.config/systemd/user - cat > ~/.config/systemd/user/theplug.service <