# Unit 05 — Flask Application ## Goal Main Flask server wiring together the database, profile generator, utils, and core adapters. ## File - `server/app.py` --- ## Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/` | none | 302 redirect → `/activate` | | GET | `/activate` | none | Serve `portal/index.html` | | POST | `/api/provision` | none | Generate + provision eSIM | | GET | `/api/profile/` | none | Download profile bytes | | GET | `/api/export` | none | JSON export of all subscribers | | GET | `/api/status` | none | Health check | --- ## `POST /api/provision` — Full Flow Request body (JSON): ```json {"mac_address": "aa:bb:cc:dd:ee:ff"} ``` Steps: 1. Parse JSON body; if `mac_address` missing/empty, resolve from server-side ARP (`/proc/net/arp` lookup by client IP) 2. `normalize_mac()` + `validate_mac()` → 400 if invalid 3. `check_rate_limit(conn, mac, window_hours)` → 429 if exceeded 4. `generate_imsi(mcc, mnc)` 5. `generate_ki()` 6. `generate_iccid()` 7. `derive_opc(ki, op_bytes)` 8. `create_esim_profile(...)` → `profile_bytes` 9. `add_subscriber(conn, imsi, iccid, ki_hex, opc_hex, mac)` → `sub_id` 10. Store `profile_bytes` in memory cache `{sub_id: bytes}` (or temp file) 11. If `core_adapter` configured: attempt `add_subscriber(imsi, ki_hex, opc_hex)`; log failure, don't abort 12. Return 200: ```json { "id": 1, "imsi": "302720XXXXXXXXXX", "iccid": "8910302720XXXXXXXXX", "profile_url": "/api/profile/1" } ``` Error responses: - 400: `{"error": "Invalid MAC address"}` - 429: `{"error": "Rate limit exceeded. One profile per device per hour."}` - 500: `{"error": "Profile generation failed"}` --- ## `GET /api/profile/` — Profile Download - Look up subscriber by `id` in DB - Retrieve profile bytes from in-memory cache (keyed by `sub_id`) - 404 if not found or cache expired - Response: `Content-Type: application/octet-stream`, `Content-Disposition: attachment; filename=esim-profile.bin` --- ## `GET /api/export` — Subscriber Export Response: ```json { "exported_at": "2025-02-11T10:30:00Z", "count": 123, "subscribers": [ { "imsi": "302720123456789", "ki": "00112233445566778899aabbccddeeff", "opc": "63bfa50ee6523365ff14c1f45f88737d", "iccid": "8910302720123456789", "mac_address": "aa:bb:cc:dd:ee:ff", "created_at": "2025-02-11T09:15:00Z", "synced_to_core": false } ] } ``` --- ## `GET /api/status` — Health ```json { "status": "ok", "subscriber_count": 42, "uptime_seconds": 3600, "core_adapter": "open5gs" } ``` --- ## Application Setup ```python import os, logging from flask import Flask, request, jsonify, send_file, redirect from server.database import init_db, get_connection, ... from server.profile_generator import ... from server.utils import load_config, ... from server.core_adapters import get_adapter CONFIG_PATH = os.environ.get("CONFIG_PATH", "config.yaml") def create_app(config_path=CONFIG_PATH): app = Flask(__name__, static_folder="../portal", static_url_path="") cfg = load_config(config_path) db_path = cfg["database"]["path"] init_db(db_path) adapter = get_adapter(cfg) profile_cache = {} # {sub_id: bytes} start_time = time.time() # register routes ... return app if __name__ == "__main__": app = create_app() app.run(host=cfg["server"]["host"], port=cfg["server"]["port"]) ``` --- ## MAC Address from ARP (server-side fallback) When client doesn't send MAC: ```python def get_mac_from_arp(ip: str) -> str | None: try: with open("/proc/net/arp") as f: for line in f: parts = line.split() if len(parts) >= 4 and parts[0] == ip: return parts[3] except Exception: pass return None ``` --- ## Verification ```bash CONFIG_PATH=config.yaml python -m server.app & sleep 1 curl -s http://localhost:5000/api/status | python -m json.tool curl -s -X POST http://localhost:5000/api/provision \ -H "Content-Type: application/json" \ -d '{"mac_address":"aa:bb:cc:dd:ee:ff"}' | python -m json.tool curl -s http://localhost:5000/api/export | python -m json.tool kill %1 ```