docs: rewrite README for public release
General-purpose README for external users; tidy CI workflow comment.
This commit is contained in:
parent
6c27b75208
commit
bc631bc112
2 changed files with 124 additions and 40 deletions
|
|
@ -1,16 +1,5 @@
|
||||||
# .forgejo/workflows/gitleaks.yml
|
|
||||||
#
|
|
||||||
# Gitleaks secret-scan workflow.
|
# Gitleaks secret-scan workflow.
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# Runs gitleaks on every push and pull request to catch committed secrets.
|
# Runs gitleaks on every push and pull request to catch committed secrets.
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
name: gitleaks
|
name: gitleaks
|
||||||
|
|
||||||
|
|
|
||||||
153
README.md
153
README.md
|
|
@ -1,47 +1,95 @@
|
||||||
# adacam-api
|
# 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
|
The API runs locally on the device and exposes a small HTTP surface that a
|
||||||
- Reads GPS data from Redis (`GNSSFusion30Hz`)
|
companion app (or any HTTP client) can use to read GPS, list recent landmark
|
||||||
- Stores landmark detections in SQLite
|
detections, check device status, configure WiFi/SSH, and optionally forward
|
||||||
- Forwards detections to AdaMaps API (with offline queuing)
|
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
|
## Endpoints
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Auth | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|------|-------------|
|
||||||
| GET | `/api/1/landmarks/last/{N}` | Last N detections |
|
| GET | `/api/1/landmarks/last/{N}` | no | Last N detections |
|
||||||
| POST | `/api/1/landmarks` | Ingest new detection |
|
| POST | `/api/1/landmarks` | yes | Ingest a new detection |
|
||||||
| GET | `/api/1/gnssConcise/latestValid` | Current GPS fix |
|
| GET | `/api/1/gnssConcise/latestValid` | no | Current GPS fix |
|
||||||
| GET | `/api/1/status` | Device status |
|
| GET | `/api/1/status` | no | Device status (firmware, uptime, GPS lock) |
|
||||||
| GET | `/api/1/deviceinfo` | Device identity |
|
| GET | `/api/1/deviceinfo` | no | Device identity |
|
||||||
| GET | `/api/1/recording/frames/latest` | Latest frame path |
|
| 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
|
```bash
|
||||||
./install.sh
|
./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
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
"device_id": "auto-generated UUID",
|
"device_id": "auto-generated UUID",
|
||||||
"adamaps_key": "<your-adamaps-ingest-key>",
|
"adamaps_key": "<map-ingest API key>",
|
||||||
"adamaps_api": "https://api.adamaps.org",
|
"adamaps_api": "https://api.adamaps.org",
|
||||||
"ap_interface": "wlp1s0f0",
|
"ap_interface": "wlp1s0f0",
|
||||||
"tunnel_host": "",
|
"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
|
||||||
|
`<adamaps_api>/api/ingest` with the key in an `X-AdaMaps-Key` header.
|
||||||
|
- `ap_interface` — the device's access-point network interface.
|
||||||
|
|
||||||
- Python 3.8+
|
The API key can be required from the environment instead of being stored
|
||||||
- Redis (for GPS/IMU data)
|
inline; in that case the service refuses to start until it is set.
|
||||||
- Flask, redis-py, requests
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Read-only endpoints are open; control endpoints (WiFi connect, SSH toggle,
|
||||||
|
WiGLE config, landmark ingest) require an `Authorization: Bearer <token>`
|
||||||
|
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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue