commit 8c8211b60de54611dbf2ccaf5d4d679b76bb9d09 Author: penguinehis Date: Wed Dec 10 23:27:05 2025 -0300 Doc diff --git a/README.md b/README.md new file mode 100644 index 0000000..96a04a0 --- /dev/null +++ b/README.md @@ -0,0 +1,306 @@ +# SSH Tunnel Server & Admin API + +This project is a single Go binary that: + +- Runs an SSH tunnel server (password or public-key auth) +- Tracks per-user limits, expiry and active connections +- Persists users in PostgreSQL +- Exposes a secure HTTP admin API +- Serves a modern web panel (SPA) that calls the admin API + +The admin API is protected by a static bearer token (`ADMIN_TOKEN`) and is intended to be used only by the bundled web panel or trusted automation. + +--- + +## Process overview + +1. **SSH server** + + - Listens on `config.json` → `listen` (default `:2222`) + - Supports: + - password auth (per-user) + - optional per-user public key auth + - Enforces: + - per-user `max_connections` + - per-connection bandwidth limits (up / down) + - optional expiry (`expires_at`) + +2. **Admin HTTP server** + + - Listens on `ADMIN_HTTP_ADDR` (default `127.0.0.1:8080`) + - Serves: + - static panel (from `./admin/index.html`) + - JSON API under `/api/*` + - Requires `Authorization: Bearer ` on all `/api/*` endpoints. + +3. **PostgreSQL** + + - Connection string from `PG_DSN` + - Users are stored in table `ssh_users` + - On startup: + - If `PG_DSN` is set, users are loaded from DB and override `config.json` users. + - On any create/update/delete via API: + - DB is updated + - In-memory users are reloaded + - All active connections for that user are disconnected so new config applies immediately. + +--- + +## Environment variables + +| Variable | Required | Default | Description | +|-------------------|----------|------------------|-----------------------------------------------------------------------------| +| `PG_DSN` | Optional | – | PostgreSQL DSN. If empty, runs with config-only users (no DB persistence). | +| `ADMIN_TOKEN` | Required | – | Static bearer token for admin API & panel login. | +| `ADMIN_HTTP_ADDR` | Optional | `127.0.0.1:8080` | Address for the admin HTTP server (panel + API). | + +Example: + +```bash +export PG_DSN='postgres://sshpanel:password@localhost:5432/sshpanel?sslmode=disable' +export ADMIN_TOKEN='super-long-random-token' +export ADMIN_HTTP_ADDR='0.0.0.0:8080' + +./sshpanel -config config.json +``` + +--- + +## Data model (simplified) + +The admin API works on a user model with these key fields: + +```jsonc +{ + "username": "test", + "password": "secret", // write-only; never returned by API + "max_connections": 1, // 0 = unlimited + "expires_at": "2025-12-12T22:37:00Z", // RFC3339 or empty string for "never" + "limit_mbps_up": 10, // per-connection upstream limit (Mbps) + "limit_mbps_down": 10 // per-connection downstream limit (Mbps) +} +``` + +Additional runtime field: + +- `active_conns` – current number of live SSH connections for that user (read-only, calculated in memory). + +--- + +## Authentication + +All admin API endpoints require: + +```http +Authorization: Bearer +``` + +If the header is missing or incorrect: + +- `401 Unauthorized` if token doesn’t match +- `403 Forbidden` if `ADMIN_TOKEN` is not configured on the server + +The static panel (`/`) does **not** expose data until a token is provided in the login screen; it then stores it in `localStorage` and sends it as `Authorization: Bearer ...` on each request. + +--- + +## API routes + +Base URL: `http://` + +### 1. `GET /api/users` + +List all users with current status. + +**Auth:** required (Bearer token) + +**Request:** no body + +**Response:** + +- `200 OK` with JSON array of users +- `401/403` on auth failure + +Schema: + +```jsonc +[ + { + "username": "test", + "active_conns": 1, + "max_connections": 1, + "expires_at": "2025-12-12T22:37:00Z", // or null if no expiry + "limit_mbps_up": 10, + "limit_mbps_down": 10 + } +] +``` + +**Example (curl):** + +```bash +curl -H "Authorization: Bearer $ADMIN_TOKEN" \ + http://127.0.0.1:8080/api/users +``` + +--- + +### 2. `POST /api/users/create` + +Create a new user or update an existing user (upsert). + +**Auth:** required + +**Request body:** JSON, fields: + +```jsonc +{ + "username": "test", // required + "password": "secret", // required for new user; optional on update + "max_connections": 1, // required; 0 = unlimited + "expires_at": "2025-12-12T22:37:00Z", // optional; "" = no expiry + "limit_mbps_up": 10, // required; 0 = use default or unlimited + "limit_mbps_down": 10 // required; 0 = use default or unlimited +} +``` + +Behavior: + +- If `username` **does not exist** in DB: + - `password` **must** be non-empty. + - Creates a new user row in `ssh_users`. +- If `username` **already exists**: + - If `password` is **non-empty**, it replaces the old password. + - If `password` is **missing or empty**, the existing password is kept. + - Other fields are updated. + +Side effect: + +- After a successful upsert, **all active SSH connections for that user are closed**. + New connections must use the updated credentials/limits. + +**Responses:** + +- `201 Created` on success (empty body) +- `400 Bad Request` on invalid body or missing required fields (`username`, or `password` when creating a new user) +- `401/403` on auth failure +- `500 Internal Server Error` on DB issues + +**Example – create a new user:** + +```bash +curl -X POST http://127.0.0.1:8080/api/users/create \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "password": "alicepass", + "max_connections": 2, + "expires_at": "2025-12-31T23:59:00Z", + "limit_mbps_up": 10, + "limit_mbps_down": 10 + }' +``` + +**Example – update only limits (keep password):** + +```bash +curl -X POST http://127.0.0.1:8080/api/users/create \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "max_connections": 3, + "expires_at": "2025-12-31T23:59:00Z", + "limit_mbps_up": 20, + "limit_mbps_down": 20 + }' +# password omitted => password is kept as-is +# any active sessions for alice are disconnected +``` + +**Example – change password and limits:** + +```bash +curl -X POST http://127.0.0.1:8080/api/users/create \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "password": "newpass", + "max_connections": 1, + "expires_at": "", + "limit_mbps_up": 5, + "limit_mbps_down": 5 + }' +``` + +--- + +### 3. `DELETE /api/users/delete` + +Delete a user and disconnect all their active sessions. + +**Auth:** required + +**Request:** + +- Method: `DELETE` +- Query parameter: `username` (required) + +Example URL: + +```text +/api/users/delete?username=alice +``` + +Behavior: + +- User row is deleted from `ssh_users`. +- In-memory state is reloaded. +- All active SSH connections for that user are closed. + +**Responses:** + +- `204 No Content` on success +- `400 Bad Request` if `username` is missing +- `401/403` on auth failure +- `500 Internal Server Error` on DB issues + +**Example (curl):** + +```bash +curl -X DELETE "http://127.0.0.1:8080/api/users/delete?username=alice" \ + -H "Authorization: Bearer $ADMIN_TOKEN" +``` + +--- + +### 4. `GET /` (static panel) + +Serves the SPA admin panel (`admin/index.html`). + +- No auth at HTTP level, but: + - The panel shows a **login screen** asking for `ADMIN_TOKEN`. + - The token is stored in `localStorage` and used as `Authorization: Bearer ...` for all `/api/*` calls. +- If the token is wrong, the UI shows an error and stays on the login view. + +You typically access it in a browser at: + +```text +http://127.0.0.1:8080/ +``` + +(or whatever you put in `ADMIN_HTTP_ADDR`). + +--- + +## Notes and recommendations + +- Expose `ADMIN_HTTP_ADDR` publicly **only** if you are confident the `ADMIN_TOKEN` has sufficient entropy (long, random) and you trust the network. +- For extra safety: + - Bind `ADMIN_HTTP_ADDR` to `127.0.0.1:8080` and access through an SSH tunnel or reverse proxy with TLS. + - Rotate `ADMIN_TOKEN` regularly; restart the binary after changing it. +- Any change (create, update, delete) to a user via the API immediately disconnects their current SSH sessions, so clients must reconnect. + +---