From bc631bc1128a5456a37f90a1926488ee3154416b Mon Sep 17 00:00:00 2001 From: Sulkta Date: Sun, 28 Jun 2026 12:53:13 -0700 Subject: [PATCH] docs: rewrite README for public release General-purpose README for external users; tidy CI workflow comment. --- .forgejo/workflows/gitleaks.yml | 11 --- README.md | 153 ++++++++++++++++++++++++++------ 2 files changed, 124 insertions(+), 40 deletions(-) diff --git a/.forgejo/workflows/gitleaks.yml b/.forgejo/workflows/gitleaks.yml index d910b2d..0ba89b0 100644 --- a/.forgejo/workflows/gitleaks.yml +++ b/.forgejo/workflows/gitleaks.yml @@ -1,16 +1,5 @@ -# .forgejo/workflows/gitleaks.yml -# # Gitleaks secret-scan workflow. -# -# -# # Runs gitleaks on every push and pull request to catch committed secrets. -# -# -# -# -# -# name: gitleaks diff --git a/README.md b/README.md index d7f2231..d683516 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,95 @@ # adacam-api -Clean Python Flask replacement for Hivemapper's `odc-api` — a 434k-line Node.js monolith with a filed CVE. This service runs on the **Hivemapper Bee (HDC-S)** dashcam as part of the **adacam** firmware stack. +A small, readable **Python/Flask on-device API** for dashcams in the Hivemapper +Bee (HDC-S) family. It is a clean-room, open replacement for the device's +stock `odc-api` — a large Node.js service — reduced to a few hundred lines of +Python that are easy to audit, modify, and self-host. -## What it does +> Independent open-source project. Not affiliated with, endorsed by, or +> supported by Hivemapper. "Hivemapper" and "Bee" are used only to describe +> hardware compatibility. -- Serves API endpoints for the companion Android app and adamaps-forwarder -- Reads GPS data from Redis (`GNSSFusion30Hz`) -- Stores landmark detections in SQLite -- Forwards detections to AdaMaps API (with offline queuing) +The API runs locally on the device and exposes a small HTTP surface that a +companion app (or any HTTP client) can use to read GPS, list recent landmark +detections, check device status, configure WiFi/SSH, and optionally forward +detections to a map-ingest backend. + +## Features + +- **Landmark store + forwarder** — detections are written to a local SQLite + database and forwarded to a configurable map-ingest API. Forwarding is + resilient: anything that fails while the device is offline is queued and + retried by a background thread. +- **GPS** — reads the latest fix from the device's on-disk recording database + (`odc-api.db`, `framekms` table), with a Redis fallback for live fixes. +- **Device control** — WiFi client connect/status, SSH enable/disable, and + device status/identity endpoints. +- **WiGLE** — optional wardriving status, stats, and configuration endpoints. +- **Token auth + pairing** — control endpoints require a bearer token. The + token is a high-entropy random secret stored on the device; clients obtain it + through a one-shot, physically-gated pairing window (see + [Authentication](#authentication)). ## Endpoints -| Method | Path | Description | -|--------|------|-------------| -| GET | `/api/1/landmarks/last/{N}` | Last N detections | -| POST | `/api/1/landmarks` | Ingest new detection | -| GET | `/api/1/gnssConcise/latestValid` | Current GPS fix | -| GET | `/api/1/status` | Device status | -| GET | `/api/1/deviceinfo` | Device identity | -| GET | `/api/1/recording/frames/latest` | Latest frame path | +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| GET | `/api/1/landmarks/last/{N}` | no | Last N detections | +| POST | `/api/1/landmarks` | yes | Ingest a new detection | +| GET | `/api/1/gnssConcise/latestValid` | no | Current GPS fix | +| GET | `/api/1/status` | no | Device status (firmware, uptime, GPS lock) | +| GET | `/api/1/deviceinfo` | no | Device identity | +| GET | `/api/1/recording/frames/latest` | no | Path to the most recent frame | +| GET | `/api/1/info` | no | Device serial + AP info | +| GET | `/pair` | no* | One-shot pairing — returns the API token while the window is open | +| GET | `/api/1/wifi/status` | no | WiFi client status | +| POST | `/api/1/wifi/connect` | yes | Connect to a WiFi network | +| GET | `/api/1/ssh/status` | no | SSH daemon status | +| POST | `/api/1/ssh/toggle` | yes | Enable/disable SSH | +| GET | `/api/1/wigle/status` | no | WiGLE service status | +| GET | `/api/1/wigle/stats` | no | WiGLE statistics | +| GET | `/api/1/wigle/config` | no | WiGLE config (secrets masked) | +| POST | `/api/1/wigle/config` | yes | Update WiGLE config | +| GET | `/api/1/debug/redis-keys` | yes | List Redis keys (GPS troubleshooting) | -**Note:** `/api/1/cmd` is intentionally NOT implemented — that was the CVE. +`*` `/pair` only returns the token while a pairing window is open; otherwise it +returns `403`. -## Installation +> Note: there is intentionally no remote command-execution endpoint. The stock +> firmware's `/api/1/cmd` (the source of a known CVE) is deliberately not +> implemented here. + +## Quick start + +Requirements: + +- Python 3.8+ +- Redis (optional — used as a live-GPS fallback) +- `flask`, `redis`, `requests` (see `requirements.txt`) + +Run it locally: + +```bash +pip install -r requirements.txt +python main.py # serves on 0.0.0.0:5000 +``` + +Install on a device (copies files to `/opt/adacam`, installs the pairing +helper, and enables a systemd service): ```bash ./install.sh ``` -This will: -1. Copy files to `/opt/adacam/` -2. Install Python dependencies -3. Enable and start the systemd service -4. Generate a device ID on first run - ## Configuration -Config file: `/data/adacam/config.json` +Config lives at `/data/adacam/config.json` and is created with defaults on +first start. A device ID (UUID) is generated automatically. ```json { "device_id": "auto-generated UUID", - "adamaps_key": "", + "adamaps_key": "", "adamaps_api": "https://api.adamaps.org", "ap_interface": "wlp1s0f0", "tunnel_host": "", @@ -50,12 +98,59 @@ Config file: `/data/adacam/config.json` } ``` -## Requirements +- `adamaps_api` / `adamaps_key` — the upstream map-ingest endpoint and its API + key. Point these at your own backend; detections are POSTed to + `/api/ingest` with the key in an `X-AdaMaps-Key` header. +- `ap_interface` — the device's access-point network interface. -- Python 3.8+ -- Redis (for GPS/IMU data) -- Flask, redis-py, requests +The API key can be required from the environment instead of being stored +inline; in that case the service refuses to start until it is set. + +## Authentication + +Read-only endpoints are open; control endpoints (WiFi connect, SSH toggle, +WiGLE config, landmark ingest) require an `Authorization: Bearer ` +header. + +The token is **not** derived from any public identifier. On first use the +device generates a random `secrets.token_urlsafe(32)` value, stores it at +`/data/adacam/api_token` (mode `600`), and compares it in constant time. + +Clients obtain the token through a **one-shot pairing window**: + +``` +device: adacam-pair # opens the window (physical/SSH presence) +client: GET /pair -> { serial, token, ... } # window closes on success +client: stores token; sends it on all control calls +``` + +`install.sh` opens the window once on a fresh provision so the first pair works +out of the box. To re-pair (new client, reset), run `adacam-pair` on the device +again. + +## Development + +The code is a standard Flask app factory (`adacam_api/app.py`) with route +blueprints under `adacam_api/routes/`. There are no build steps — edit and +restart. + +``` +adacam_api/ + app.py # app factory + pairing/wifi/ssh/debug routes + auth.py # bearer token + pairing window + config.py # JSON config load/save + db.py # SQLite: landmarks + offline forward queue + forwarder.py # background map-ingest forwarder with retry + redis_client.py # GPS reader (SQLite primary, Redis fallback) + routes/ # landmarks, gnss, status, frames, wigle blueprints +``` + +## Contributing + +Issues and pull requests are welcome. Please keep changes small and focused, +match the existing style, and avoid adding heavy dependencies — the goal is a +service small enough to read in one sitting. ## License -MIT +MIT — see [LICENSE](LICENSE).