From 1d9668ee3450d1f37edeabed5c6b1fa53c191862 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Sun, 5 Jul 2026 22:14:39 -0400 Subject: [PATCH] feat: rveng M4 challenges + SQLite progress, M5 self-host + docs M4: - Add 01-read-the-hex (hex-reading) and 02-find-the-entry (elf-anatomy) gradeable challenges over the gate binary, completing the five-module pedagogy - Swap in-memory progress for a SQLite ProgressStore behind the existing Protocol; routes untouched, migration-free, thread-safe M5: - Self-host Docker: prod (nginx + uvicorn) and dev (nginx + Vite HMR) compose, justfile, Dockerfiles, and nginx configs; zero-config clone-and-run - learn/ teaching docs (00-04) and public README - One-shot install.sh and uninstall.sh --- PROJECTS/advanced/rveng/.dockerignore | 18 ++ PROJECTS/advanced/rveng/.gitignore | 2 + PROJECTS/advanced/rveng/README.md | 160 ++++++++++++ .../challenges/01-read-the-hex/challenge.json | 7 + .../rveng/challenges/01-read-the-hex/source.c | 23 ++ .../rveng/challenges/01-read-the-hex/target | Bin 0 -> 15904 bytes .../02-find-the-entry/challenge.json | 7 + .../challenges/02-find-the-entry/source.c | 23 ++ .../rveng/challenges/02-find-the-entry/target | Bin 0 -> 15904 bytes PROJECTS/advanced/rveng/compose.yml | 39 +++ PROJECTS/advanced/rveng/dev.compose.yml | 61 +++++ PROJECTS/advanced/rveng/frontend/.gitignore | 1 + .../rveng/infra/docker/api.dockerfile | 28 +++ PROJECTS/advanced/rveng/infra/docker/vite.dev | 12 + .../advanced/rveng/infra/docker/vite.prod | 21 ++ PROJECTS/advanced/rveng/infra/nginx/dev.nginx | 31 +++ .../advanced/rveng/infra/nginx/prod.nginx | 28 +++ PROJECTS/advanced/rveng/install.sh | 236 ++++++++++++++++++ PROJECTS/advanced/rveng/justfile | 111 ++++++++ PROJECTS/advanced/rveng/learn/00-OVERVIEW.md | 108 ++++++++ PROJECTS/advanced/rveng/learn/01-CONCEPTS.md | 175 +++++++++++++ .../advanced/rveng/learn/02-ARCHITECTURE.md | 166 ++++++++++++ .../advanced/rveng/learn/03-IMPLEMENTATION.md | 195 +++++++++++++++ .../advanced/rveng/learn/04-CHALLENGES.md | 141 +++++++++++ PROJECTS/advanced/rveng/src/rveng/api/app.py | 4 +- .../advanced/rveng/src/rveng/api/store.py | 45 +++- PROJECTS/advanced/rveng/tests/test_api.py | 53 +++- .../advanced/rveng/tests/test_progress.py | 54 ++++ PROJECTS/advanced/rveng/uninstall.sh | 58 +++++ 29 files changed, 1799 insertions(+), 8 deletions(-) create mode 100644 PROJECTS/advanced/rveng/.dockerignore create mode 100644 PROJECTS/advanced/rveng/challenges/01-read-the-hex/challenge.json create mode 100644 PROJECTS/advanced/rveng/challenges/01-read-the-hex/source.c create mode 100755 PROJECTS/advanced/rveng/challenges/01-read-the-hex/target create mode 100644 PROJECTS/advanced/rveng/challenges/02-find-the-entry/challenge.json create mode 100644 PROJECTS/advanced/rveng/challenges/02-find-the-entry/source.c create mode 100755 PROJECTS/advanced/rveng/challenges/02-find-the-entry/target create mode 100644 PROJECTS/advanced/rveng/compose.yml create mode 100644 PROJECTS/advanced/rveng/dev.compose.yml create mode 100644 PROJECTS/advanced/rveng/infra/docker/api.dockerfile create mode 100644 PROJECTS/advanced/rveng/infra/docker/vite.dev create mode 100644 PROJECTS/advanced/rveng/infra/docker/vite.prod create mode 100644 PROJECTS/advanced/rveng/infra/nginx/dev.nginx create mode 100644 PROJECTS/advanced/rveng/infra/nginx/prod.nginx create mode 100755 PROJECTS/advanced/rveng/install.sh create mode 100644 PROJECTS/advanced/rveng/justfile create mode 100644 PROJECTS/advanced/rveng/learn/00-OVERVIEW.md create mode 100644 PROJECTS/advanced/rveng/learn/01-CONCEPTS.md create mode 100644 PROJECTS/advanced/rveng/learn/02-ARCHITECTURE.md create mode 100644 PROJECTS/advanced/rveng/learn/03-IMPLEMENTATION.md create mode 100644 PROJECTS/advanced/rveng/learn/04-CHALLENGES.md create mode 100644 PROJECTS/advanced/rveng/tests/test_progress.py create mode 100755 PROJECTS/advanced/rveng/uninstall.sh diff --git a/PROJECTS/advanced/rveng/.dockerignore b/PROJECTS/advanced/rveng/.dockerignore new file mode 100644 index 00000000..069e90a9 --- /dev/null +++ b/PROJECTS/advanced/rveng/.dockerignore @@ -0,0 +1,18 @@ +# ©AngelaMos | 2026 +# .dockerignore + +.git +.venv +data +**/__pycache__ +**/*.pyc +.pytest_cache +**/.pytest_cache + +frontend/node_modules +frontend/dist +frontend/.vite +frontend/.biome_cache + +docs/plans +docs/context diff --git a/PROJECTS/advanced/rveng/.gitignore b/PROJECTS/advanced/rveng/.gitignore index 173d0337..b76929ba 100644 --- a/PROJECTS/advanced/rveng/.gitignore +++ b/PROJECTS/advanced/rveng/.gitignore @@ -8,3 +8,5 @@ __pycache__/ .pytest_cache/ .venv/ *.pyc + +data/ diff --git a/PROJECTS/advanced/rveng/README.md b/PROJECTS/advanced/rveng/README.md index e69de29b..0610a517 100644 --- a/PROJECTS/advanced/rveng/README.md +++ b/PROJECTS/advanced/rveng/README.md @@ -0,0 +1,160 @@ + + + +``` + ______ _____ ____ ____ _ + / ___/ | / / _ \/ __ \/ __ `/ + / / | |/ / __/ / / / /_/ / +/_/ |___/\___/_/ /_/\__, / + /____/ +``` + +[![Cybersecurity Projects](https://img.shields.io/badge/Cybersecurity--Projects-Project%20%2337-red?style=flat&logo=github)](https://github.com/CarterPerez-dev/Cybersecurity-Projects/tree/main/PROJECTS/advanced/rveng) +[![Python](https://img.shields.io/badge/Python-3.13-3776AB?style=flat&logo=python&logoColor=white)](https://www.python.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-009688?style=flat&logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com) +[![React](https://img.shields.io/badge/React-19-61DAFB?style=flat&logo=react&logoColor=black)](https://react.dev) +[![capstone](https://img.shields.io/badge/disassembler-capstone-6d4aff?style=flat)](https://www.capstone-engine.org) +[![No execution](https://img.shields.io/badge/binaries-never%20executed-2ea043?style=flat)](#it-never-runs-a-binary) +[![License: AGPLv3](https://img.shields.io/badge/License-AGPL_v3-purple.svg)](https://www.gnu.org/licenses/agpl-3.0) + +> An interactive reverse-engineering learning platform. It hands you a real compiled binary, asks a concrete question about it, gives you an in-browser hex viewer, disassembler, section map, and string scanner to answer it, and grades your answer. Only when you are right does it reveal the original C source, so you connect the machine code you just read back to the code that produced it. One framework-free Python analysis engine wears three faces: a web app, a read-only HTTP API, and an embeddable library. + +## Why a reverse-engineering platform + +A pile of worksheets and pre-compiled binaries can walk you through reverse engineering, but it cannot check your work and it cannot become anything more. rveng keeps the solve-then-reveal loop and makes it a real system. The engine that parses ELF, drives the disassembler, resolves imports, and grades answers is the core. The web app turns the worksheet into an interactive lab: an in-browser hex viewer, live disassembly, a section map, and a gradeable challenge runner. The engine and its lesson content are decoupled from the web framework, so they embed into a larger application as a standalone reverse-engineering feature. One core, three consumers, curated content. + +## It never runs a binary + +The single load-bearing decision, and the reason a web app that eats binaries is safe: the backend never executes any binary. Every operation is reading and parsing bytes. + +- Hex dump, ELF header parse, section walk, symbol read, and string scan are all pure byte reads. +- Disassembly is decoding, not running. capstone reads instruction bytes and returns their text form; it never transfers control to the decoded code. +- Patch challenges are graded by a static byte diff against a known-good patched target. The patched binary is never executed. + +Challenge binaries are curated and pre-compiled and shipped as static assets; nobody uploads an executable to run. So there is no arbitrary-code-execution surface, no sandbox to escape, and no resource-exhaustion path through a hostile binary, because nothing is ever run. This is a hard constraint, not a preference. Any feature that would require executing a binary is out of scope until it is redesigned against this posture, most likely by moving execution to an isolated, disposable sandbox that is explicitly not the analysis backend. + +## Features + +**The engine** +- A hand-rolled ELF64 parser: header, section table, and symbol table read straight from raw bytes +- x86-64 disassembly via capstone in Intel syntax, annotated with comparisons, conditional branches, call targets, and RIP-relative data references +- Import resolution through the PLT, walking `.plt`, `.rela.plt`, `.dynsym`, and `.dynstr` the way the loader would, so a bare `call 0x401050` becomes `call atoi` +- Cross-references (who calls this, what data it touches) and a basic-block control-flow graph for a single function +- Function discovery in stripped binaries by scanning executable sections for the standard prologue, so a symbol-stripped binary is still navigable + +**The platform** +- Six curated challenges over one sample binary, spanning the five core reverse-engineering skills plus a stripped variant +- Solve-then-reveal grading in three machine-checkable categories: found-value, identified-symbol, and patched-bytes +- Progress persisted in SQLite behind a swappable interface, so it drops into a host application's own store without touching the engine +- A React web app and a read-only FastAPI, served together for one-command self-hosting + +## Quick Start + +rveng self-hosts on localhost with Docker. No account, no secret, no external service, nothing to configure. + +```bash +curl -fsSL https://angelamos.com/rveng/install.sh | bash +# then open http://localhost:8790 +``` + +One command takes a fresh machine to the app built and running: it installs Docker if it is missing, builds the engine image and the React app, brings the stack up, and waits until it answers. The first thing you do is open the browser. + +Already have the repo cloned? Run it straight from the project directory: + +```bash +just up # build and serve on http://localhost:8790 +just dev-up # hot-reload dev stack on http://localhost:8791 +just test # engine tests, no server (uv run pytest -q) +just typecheck # frontend type check, no server +just down # stop +``` + +> [!TIP] +> This project uses [`just`](https://github.com/casey/just) as a command runner. Type `just` to see every recipe grouped by area: `dev` (dockerized Vite hot-reload), `prod` (the self-host stack), `verify` (tests and type check), and `cleanup`. +> +> Install: `curl -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin` + +## Architecture + +One analysis engine with three faces. The engine is framework-free Python that knows nothing about HTTP or React. A thin FastAPI layer adapts it to the web. A React app consumes that API. Progress lives behind a small interface so the whole thing embeds into a larger application without dragging a web framework along. + +``` + +--------------------------+ + | rveng/engine/ (pure) | + | elf disasm plt xref cfg | + | hex strings patch discover| + | challenge (grading) | + +------------+--------------+ + | + +-----------------+-----------------+ + | | + +-----+------+ +------+------+ + | HTTP API | | library | + | FastAPI | | (import it)| + +-----+------+ +-------------+ + | + +-----+------+ + | React app | + +------------+ +``` + +In production, nginx serves the built frontend and proxies `/api` to the engine container, which stays a pure API and never learns to serve a SPA. Development mirrors that with nginx fronting the Vite dev server for hot reload. The two stacks use structural-literal project names (`rveng` and `rveng-dev`), so they are namespace-isolated by construction and never collide. + +``` +PROD (compose.yml, "rveng") DEV (dev.compose.yml, "rveng-dev") + + browser :8790 browser :8791 + | | + [ nginx ] serves dist [ nginx ] --> [ vite HMR ] + | /api | /api + [ api ] uvicorn [ api ] uvicorn --reload + | | + rveng_data (sqlite progress) rveng_data_dev +``` + +## Project Structure + +``` +rveng/ +├── compose.yml # prod self-host: nginx + api (name: rveng) +├── dev.compose.yml # dev: nginx + vite HMR + api --reload +├── justfile # dev / prod / verify / cleanup recipes +├── install.sh # one-shot curl|bash: installs Docker, builds, runs +├── uninstall.sh # tears the stacks down and removes the cache +├── infra/ +│ ├── docker/ # api.dockerfile, vite.dev, vite.prod (multistage -> nginx) +│ └── nginx/ # dev.nginx (fronts Vite HMR), prod.nginx (serves dist) +├── src/rveng/ +│ ├── engine/ # the pure analysis core (no HTTP, no framework) +│ │ ├── elf.py # hand-rolled ELF64 header / sections / symbols +│ │ ├── disasm.py # capstone x86-64 decode + annotation +│ │ ├── plt.py # PLT / GOT import resolution +│ │ ├── xref.py # cross-references from decoded instructions +│ │ ├── cfg.py # basic-block control-flow graph +│ │ ├── discover.py # stripped-binary function discovery +│ │ ├── hex.py strings.py patch.py # dump, string scan, byte diff +│ │ └── challenge.py # the challenge model and solve-then-reveal grader +│ └── api/ # thin FastAPI adapter over the engine +│ ├── app.py # create_app() and every read-only route +│ ├── store.py # challenge loader + ProgressStore (in-memory / sqlite) +│ ├── schemas.py limits.py middleware.py server.py +├── challenges/ # the six curated challenges (target + source + answer) +├── frontend/ # the React face (self-contained, extractable) +└── learn/ # the teaching track +``` + +## Learn + +This project ships a full teaching track. Read it in order, or jump to what you need. + +| Doc | What it covers | +|-----|----------------| +| [`learn/00-OVERVIEW.md`](learn/00-OVERVIEW.md) | What rveng is, the no-execution posture, and a quick tour | +| [`learn/01-CONCEPTS.md`](learn/01-CONCEPTS.md) | Reverse-engineering theory: static analysis, ELF, symbols, the PLT, patching, grounded in real analysis | +| [`learn/02-ARCHITECTURE.md`](learn/02-ARCHITECTURE.md) | The one-engine-three-faces design and how a request flows, with diagrams | +| [`learn/03-IMPLEMENTATION.md`](learn/03-IMPLEMENTATION.md) | A code walkthrough of every engine module against the sample binary | +| [`learn/04-CHALLENGES.md`](learn/04-CHALLENGES.md) | The six challenges, what each teaches, and how to add your own | + +## License + +[AGPL 3.0](LICENSE). diff --git a/PROJECTS/advanced/rveng/challenges/01-read-the-hex/challenge.json b/PROJECTS/advanced/rveng/challenges/01-read-the-hex/challenge.json new file mode 100644 index 00000000..8902224a --- /dev/null +++ b/PROJECTS/advanced/rveng/challenges/01-read-the-hex/challenge.json @@ -0,0 +1,7 @@ +{ + "id": "01-read-the-hex", + "module": "hex-reading", + "title": "Read the hex", + "mission": "This binary hides a secret string in its read-only data. Open the hex viewer and read the ASCII column on the right to recover it. Submit the string exactly as it appears.", + "answer": { "category": "found_value", "expected": "the_flag_is_here" } +} diff --git a/PROJECTS/advanced/rveng/challenges/01-read-the-hex/source.c b/PROJECTS/advanced/rveng/challenges/01-read-the-hex/source.c new file mode 100644 index 00000000..1c42d31b --- /dev/null +++ b/PROJECTS/advanced/rveng/challenges/01-read-the-hex/source.c @@ -0,0 +1,23 @@ +#include +#include + +int check(int n) { + if (n == 1337) { + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + char *secret = "the_flag_is_here"; + int n = 0; + if (argc > 1) { + n = atoi(argv[1]); + } + if (check(n)) { + printf("unlocked: %s\n", secret); + } else { + printf("wrong number\n"); + } + return 0; +} diff --git a/PROJECTS/advanced/rveng/challenges/01-read-the-hex/target b/PROJECTS/advanced/rveng/challenges/01-read-the-hex/target new file mode 100755 index 0000000000000000000000000000000000000000..bc1436b17427672fa70b3c2a0d73bb41cf236307 GIT binary patch literal 15904 zcmeHOeQXrh5r22a*deifhC&=Dupp4!Dn4v576Ae4k2CAafD0xes?zoPZtYv;zI3;j z*l9^f-47iom7_+Ls(z(^^y8nZsG<^WrPhflR-#IER8{4VO0g;;nF6V!rWHvot~2k= zoYxy$P5Va`sXNl{yqPz@dGqG&?!CQz@3~liZ#WbZOkr_EpfnWGrAnQJc)3H0q&pz$ zg)X*FU1^VTr{8iDSpHAr93WgPIc}x=GjFH%m~(rd1{~(|!xCQ}r>MQv#;=yf zjd_R~z;@9GUTeoroDt#kVhVNM`j3H(>BjoM{OD)z9sKtDZ9o6ct92*e3^iq5{M}FX zq@3Z7_C2XYOUlU<$6Ce?bhLD|M+@2LUg-yYu|Ft&D7qi4)~{fRepb?NT}2-Qy#|M$ zFvlzT5xJsU5SE*DglR&*;%33M@~)Y-oQ%ljos2soP>sFo5Xbuay1LE1(Y?|AmGZuJ zK#r!f8A6)SQW)}A1v$VYEOvp0$E~>8xev38w-4$rzxZ_sEi4W~8R_~_U2=@qo5MMe zp06Egs4M##5NODyy4ItoTSw|69z8vZq&_$W83-~EWFW{ukbxirK?Z^h1R3}!WWboX zU29yd{=*(2j4w^O;qt68akX~35?Xox>rhqR{aZLUY}cWLauVxr&Xs|7zlt(WrEktk z`4=eTRQcu<$ZdD}N^gADDE--(xUn!W7;Bqqd(D`cNi>41jgNupyCczt?O#NH(*Lwq z>=EKhGfb$B!+3T#TtO=g^S)oBqC7V;IX&V9=;Bw>{`t2hD=iqWynWPosxQ<-KT5HMnb;zBY2w`N(6gJ2i?KOl zVhV->>SpGDdaqnA#TGBd7D4zX1Offt3N{Ny=^D5=D_zXUCgz_4+glBw9Z=f&GoX-F zufb5tF!8{7E};P}^NtC!Wq{GoxCjm>#zCBDAII5PoCDZ(9q0Ete;Ow=R^+1HgxX ze;)Xzcht*gJMiluBjGnsq|h^`L})A$+VVh6?PRDXg5T)y8~=6iwG$11R<>2&^U(HB zV9Vd`@Y`Q29A`nN0~hs?-uk9bHQfJn?Rjx@^Px}f+a=v1?kpUSLX6wcuCqS!<#2c1 z+Q%Uf5V3v?jzOq@7I>vTJgy8th2I4+UThn`-6G+)zpyL?ryv7C27(L(83-~EWFW{u zkbxirK?Z^hd^9tl!v|QT2yyTG6zU(q6B4bHu2Yh@LG}-k%!~1hXT-`VglE*A~h){HWFvXYcl6)T_?Yz z8Dzlkn7FPJ=|RPWR;|kt>lY{cq>w__mXN)~DY~dcM9vl!W6S5)TPnAArQ;gV$9E|9PP6 zRaV03b8uk0e7~^%D^LiD4T7(K*8czsEA@A^e*IYbX%x*nbaqkUEBX1QwBIQF<9Su0 zB7)b$vy(qc`v(Qz2duvX1&qm0han*v+*M7KAFf{q`jzarfIb5DoKLpjiT2_8%JYKt z&8zr1M1K738lcx0?BS1rzA@Yc+c9*HR)_@ZMFW789{;mJDu6x$88ziDx0wjV4298 zqp9q$l`<1oyYsSK@I<=nK6TEjh<7&Jl21*tFzxc z+1qq;om`^^M(P~B!C+YDl%>$FztlpT0#a+TMd@in^>=Q6@cqyTMQsc zg{+yhGKrKe%)XOgm2fgzN zW(qr>6Vfng6_O&F7|(!H#ofHBdCJZgoNQ*9U_u@A#zFv zl<_>DTkzqzeSiN;gzqAUJU`S3V2~a~oJ0EYUjrTH1mly$&+mv5dEQC2GFC1l|0*0_ zyheD9@H!fKh}7~s1**RRhZoPkXG3K81>b-%o?U*|hv)hKjYeg`^I*28w6Z9^Pk7Eh z|1JwnnY30cRl$!}^P{El=!B^&|_+SEJJ0&g2tiJ^nc +#include + +int check(int n) { + if (n == 1337) { + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + char *secret = "the_flag_is_here"; + int n = 0; + if (argc > 1) { + n = atoi(argv[1]); + } + if (check(n)) { + printf("unlocked: %s\n", secret); + } else { + printf("wrong number\n"); + } + return 0; +} diff --git a/PROJECTS/advanced/rveng/challenges/02-find-the-entry/target b/PROJECTS/advanced/rveng/challenges/02-find-the-entry/target new file mode 100755 index 0000000000000000000000000000000000000000..bc1436b17427672fa70b3c2a0d73bb41cf236307 GIT binary patch literal 15904 zcmeHOeQXrh5r22a*deifhC&=Dupp4!Dn4v576Ae4k2CAafD0xes?zoPZtYv;zI3;j z*l9^f-47iom7_+Ls(z(^^y8nZsG<^WrPhflR-#IER8{4VO0g;;nF6V!rWHvot~2k= zoYxy$P5Va`sXNl{yqPz@dGqG&?!CQz@3~liZ#WbZOkr_EpfnWGrAnQJc)3H0q&pz$ zg)X*FU1^VTr{8iDSpHAr93WgPIc}x=GjFH%m~(rd1{~(|!xCQ}r>MQv#;=yf zjd_R~z;@9GUTeoroDt#kVhVNM`j3H(>BjoM{OD)z9sKtDZ9o6ct92*e3^iq5{M}FX zq@3Z7_C2XYOUlU<$6Ce?bhLD|M+@2LUg-yYu|Ft&D7qi4)~{fRepb?NT}2-Qy#|M$ zFvlzT5xJsU5SE*DglR&*;%33M@~)Y-oQ%ljos2soP>sFo5Xbuay1LE1(Y?|AmGZuJ zK#r!f8A6)SQW)}A1v$VYEOvp0$E~>8xev38w-4$rzxZ_sEi4W~8R_~_U2=@qo5MMe zp06Egs4M##5NODyy4ItoTSw|69z8vZq&_$W83-~EWFW{ukbxirK?Z^h1R3}!WWboX zU29yd{=*(2j4w^O;qt68akX~35?Xox>rhqR{aZLUY}cWLauVxr&Xs|7zlt(WrEktk z`4=eTRQcu<$ZdD}N^gADDE--(xUn!W7;Bqqd(D`cNi>41jgNupyCczt?O#NH(*Lwq z>=EKhGfb$B!+3T#TtO=g^S)oBqC7V;IX&V9=;Bw>{`t2hD=iqWynWPosxQ<-KT5HMnb;zBY2w`N(6gJ2i?KOl zVhV->>SpGDdaqnA#TGBd7D4zX1Offt3N{Ny=^D5=D_zXUCgz_4+glBw9Z=f&GoX-F zufb5tF!8{7E};P}^NtC!Wq{GoxCjm>#zCBDAII5PoCDZ(9q0Ete;Ow=R^+1HgxX ze;)Xzcht*gJMiluBjGnsq|h^`L})A$+VVh6?PRDXg5T)y8~=6iwG$11R<>2&^U(HB zV9Vd`@Y`Q29A`nN0~hs?-uk9bHQfJn?Rjx@^Px}f+a=v1?kpUSLX6wcuCqS!<#2c1 z+Q%Uf5V3v?jzOq@7I>vTJgy8th2I4+UThn`-6G+)zpyL?ryv7C27(L(83-~EWFW{u zkbxirK?Z^hd^9tl!v|QT2yyTG6zU(q6B4bHu2Yh@LG}-k%!~1hXT-`VglE*A~h){HWFvXYcl6)T_?Yz z8Dzlkn7FPJ=|RPWR;|kt>lY{cq>w__mXN)~DY~dcM9vl!W6S5)TPnAArQ;gV$9E|9PP6 zRaV03b8uk0e7~^%D^LiD4T7(K*8czsEA@A^e*IYbX%x*nbaqkUEBX1QwBIQF<9Su0 zB7)b$vy(qc`v(Qz2duvX1&qm0han*v+*M7KAFf{q`jzarfIb5DoKLpjiT2_8%JYKt z&8zr1M1K738lcx0?BS1rzA@Yc+c9*HR)_@ZMFW789{;mJDu6x$88ziDx0wjV4298 zqp9q$l`<1oyYsSK@I<=nK6TEjh<7&Jl21*tFzxc z+1qq;om`^^M(P~B!C+YDl%>$FztlpT0#a+TMd@in^>=Q6@cqyTMQsc zg{+yhGKrKe%)XOgm2fgzN zW(qr>6Vfng6_O&F7|(!H#ofHBdCJZgoNQ*9U_u@A#zFv zl<_>DTkzqzeSiN;gzqAUJU`S3V2~a~oJ0EYUjrTH1mly$&+mv5dEQC2GFC1l|0*0_ zyheD9@H!fKh}7~s1**RRhZoPkXG3K81>b-%o?U*|hv)hKjYeg`^I*28w6Z9^Pk7Eh z|1JwnnY30cRl$!}^P{El=!B^&|_+SEJJ0&g2tiJ^nc&2; } +ok() { printf '%s\n' " ${GREEN}+${RESET} $*" >&2; } +warn() { printf '%s\n' " ${YELLOW}!${RESET} $*" >&2; } +die() { printf '%s\n' " ${RED}x $*${RESET}" >&2; exit 1; } +header(){ printf '\n%s\n\n' "${BOLD}${CYAN}--- $* ---${RESET}" >&2; } +have() { command -v "$1" >/dev/null 2>&1; } + +trap 'printf "%s\n" "${RED}x install failed${RESET}" >&2' ERR + +banner() { + printf '%s' "${CYAN}${BOLD}" >&2 + cat >&2 <<'ART' + _ __ __ __ ___ _ __ __ _ + | '__/ \ / // -_)| '_ \ / _` | + |_| \_/\_/ \___||_.__/ \__, | + |___/ +ART + printf '%s\n' "${RESET}" >&2 + printf '%s\n' " ${DIM}${TAGLINE}${RESET}" >&2 +} + +# ============================================================================ +# Privilege + package-manager fan +# ============================================================================ +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + if have sudo; then SUDO="sudo"; fi +fi + +pkg_install() { + if have apt-get; then $SUDO apt-get update -y || warn "apt update had errors; continuing" + $SUDO apt-get install -y --no-install-recommends "$@" + elif have dnf; then $SUDO dnf install -y "$@" + elif have pacman; then $SUDO pacman -S --needed --noconfirm "$@" + elif have zypper; then $SUDO zypper install -y "$@" + elif have apk; then $SUDO apk add "$@" + elif have brew; then brew install "$@" + else die "no known package manager; install manually: $*"; fi +} + +# ============================================================================ +# Args +# ============================================================================ +usage() { + cat >&2 </dev/null || warn "pull failed; using existing clone" + else + info "cloning ${REPO_URL}" + git clone --depth 1 --branch "$DEFAULT_BRANCH" --quiet "$REPO_URL" "$cache" \ + || die "clone failed from ${REPO_URL}" + fi + printf '%s\n' "$cache/$PROJECT_SUBDIR" +} + +# ============================================================================ +# Docker — the only real dependency; install it if missing +# ============================================================================ +ensure_docker() { + if ! have docker; then + if [ "$OS" = "darwin" ]; then + die "Docker not found. Install Docker Desktop for Mac, then re-run." + fi + info "installing Docker via get.docker.com" + download_docker + fi + if ! docker info >/dev/null 2>&1; then + if have systemctl; then + info "starting the Docker daemon" + $SUDO systemctl start docker 2>/dev/null || true + fi + fi + docker info >/dev/null 2>&1 || die "Docker is installed but the daemon is not running; start it and re-run" + if ! docker compose version >/dev/null 2>&1; then + info "installing the Docker Compose plugin" + pkg_install docker-compose-plugin || die "install the Docker Compose v2 plugin, then re-run" + fi + ok "docker $(docker --version | awk '{print $3}' | tr -d ,)" +} + +download_docker() { + local tmp + tmp="$(mktemp)" + if have curl; then curl -fsSL https://get.docker.com -o "$tmp" + elif have wget; then wget -qO "$tmp" https://get.docker.com + else die "need curl or wget to install Docker"; fi + $SUDO sh "$tmp" || { rm -f "$tmp"; die "Docker install failed"; } + rm -f "$tmp" + if [ -n "$SUDO" ] && have usermod; then + $SUDO usermod -aG docker "$(id -un)" 2>/dev/null || true + fi +} + +# ============================================================================ +# Compose wrapper — run as the current user against the prod stack +# ============================================================================ +compose() { + NGINX_HOST_PORT="$HOST_PORT" docker compose "$@" +} + +http_probe() { + if have curl; then curl -fsS "$1" >/dev/null 2>&1 + elif have wget; then wget -q -O /dev/null "$1" 2>/dev/null + else return 1; fi +} + +wait_healthy() { + local url="http://localhost:${HOST_PORT}/api/challenges" i + for i in $(seq 1 90); do + if http_probe "$url"; then return 0; fi + sleep 2 + done + return 1 +} + +# ============================================================================ +# Main +# ============================================================================ +main() { + banner + ensure_docker + + header "Fetching rveng" + PROJECT="$(resolve_project)" + [ -d "$PROJECT" ] || die "could not locate the rveng project directory" + cd "$PROJECT" + ok "project at $PROJECT" + + header "Building and starting (this compiles the frontend and engine image)" + compose up -d --build + + header "Waiting for the app" + if wait_healthy; then + ok "rveng is answering on port ${HOST_PORT}" + else + warn "app did not answer within the timeout; check 'docker compose logs'" + fi + + printf '\n%s\n\n' " ${GREEN}${BOLD}rveng is running.${RESET}" >&2 + cat >&2 <