08_PROTOCOL.md 4.8 KB

Protocol v1

Overview

The interface between client and Manage server. Anyone using the bundled client doesn't need this document — it's for custom clients, for debugging, and for troubleshooting with curl.

Base URL: <MANAGE_SERVER_URL>/api/v1/

Note: the server's actual error strings are still German text, shown verbatim in the examples below — this document's prose is in English, the API contract is not.

Authentication

Every request carries two headers:

X-Manage-Instance: myproject-prod
X-Manage-Token:    e4032c4dc51e9100…

The server stores only the SHA-256 hash of the token and compares it in constant time. There is no session, no cookie and no shared password.

Error responses:

Status Meaning
401 headers missing, instance unknown, or token wrong — deliberately indistinguishable
403 instance exists but is deactivated
405 wrong HTTP method
429 too many failed authentications from this IP

Failed logins are rate-limited per IP, so instance ids can't be brute-forced. A successful login resets the counter.

Every error response has the same shape:

{
    "success": false,
    "error": "Authentifizierung fehlgeschlagen."
}

GET manifest.php

Returns the release the instance should install.

curl -s https://manage.example.org/api/v1/manifest.php \
  -H "X-Manage-Instance: myproject-prod" \
  -H "X-Manage-Token: $TOKEN"
{
    "success": true,
    "latest": "v1.3.0",
    "version": "v1.3.0",
    "package_url": "https://manage.example.org/api/v1/package.php?version=v1.3.0",
    "sha256": "70f17aae44a9afdd948de1767daa61f936bbecb52096753757791e230a22f024",
    "size": 2199,
    "published_at": "2026-08-20T09:20:43+00:00"
}

404 if no valid release is published.

package_url is built from the server configuration (MANAGE_PUBLIC_URL), not from the request's Host header. A spoofed header therefore can't redirect a client to a foreign server.

GET package.php

Returns the release ZIP.

curl -s -o release.zip \
  "https://manage.example.org/api/v1/package.php?version=v1.3.0" \
  -H "X-Manage-Instance: myproject-prod" \
  -H "X-Manage-Token: $TOKEN"

Response: application/zip with Content-Length and Cache-Control: private, no-store. No JSON on success.

400 on an invalid version format, 404 if the release doesn't exist.

The client compares size and SHA-256 against the manifest and deletes the file on mismatch. A custom client must do the same — without this check, arbitrary code gets deployed.

POST backup.php

Accepts a backup archive. multipart/form-data:

Field Required Meaning
backup yes the ZIP file
filename no backup-YYYYmmdd-HHMMSS[-N].zip; the server assigns a name if omitted
sha256 no checksum; recomputed and compared server-side
meta no JSON with trigger, file_count, source_bytes, app_version
curl -s https://manage.example.org/api/v1/backup.php \
  -H "X-Manage-Instance: myproject-prod" \
  -H "X-Manage-Token: $TOKEN" \
  -F "filename=backup-20260820-092104.zip" \
  -F "sha256=824f3f80…" \
  -F 'meta={"trigger":"cron","file_count":3}' \
  -F "backup=@backup-20260820-092104.zip"
{
    "success": true,
    "instance": "myproject-prod",
    "filename": "backup-20260820-092104.zip",
    "size": 427,
    "sha256": "824f3f80…",
    "retention": 30,
    "s3": { "enabled": false, "uploaded": false, "pending": 0 }
}

The server checks in this order: upload error code, is_uploaded_file, size limit, ZIP signature, filename pattern, checksum after storing. If the checksum doesn't match, the file is deleted again and 400 is reported.

An existing filename is never overwritten: the server appends -2, -3.

Errors during S3 archiving do not fail the upload — the local copy is stored and gets caught up later.

POST heartbeat.php

Status report. application/json:

{
    "version": "v1.3.0",
    "php_version": "8.3.6",
    "disk_free": 12884901888,
    "pending_migrations": 0,
    "last_backup_at": "2026-08-20T09:21:04+00:00"
}
{
    "success": true,
    "instance": "myproject-prod",
    "latest": "v1.3.0",
    "update_available": false,
    "server_time": "2026-08-20T09:23:11+00:00"
}

All request fields are optional; missing fields leave the server's current value unchanged. The response replaces a separate call to manifest.php for simple monitoring.

Side effect of every request

Every successfully authenticated request updates last_seen_at and the instance's latest IP. The overview in the Manage server stays current this way, even when only backups run and a heartbeat is never sent.

Next