HTTP-Routen
PlainApp stellt neben der GraphQL-API unter POST /graphql einen kleinen Satz HTTP-Endpoints bereit. Diese Endpoints verarbeiten Datei-Auslieferung, Multipart-Uploads, Zip-Streaming, DLNA-Casting, WebSocket-Ereignisse und Systemsteuerung. Alle Datei-/Upload-/Zip-Endpoints erfordern den c-id-Header (und meist einen verschlüsselten id-Query-Parameter).
Typdefinitionen
# Common headers for protected HTTP endpoints:
c-id: <client-id>
# /fs, /proxyfs, /zip/* use an encrypted "id" query parameter produced
# by the GraphQL files()/fileIds() queries. The id is opaque to the client.
# Upload endpoints additionally require an encrypted "info" multipart part:
# info = chaCha20Encrypt(token, json({ dir, size, replace, isAppFile }))
# file = <raw bytes>
# Chunked uploads use:
# info = chaCha20Encrypt(token, json({ fileId, index, size }))
# Status codes follow HTTP semantics:
# 200 OK – success
# 201 Created – upload stored
# 204 No Content – init pending password
# 400 Bad Request – missing/invalid params
# 401 Unauthorized – bad token
# 403 Forbidden – expired/invalid encrypted id
# 404 Not Found – file/entry missing
# 410 Gone – server shutting down
# 429 Too Many Requests – concurrent zip limit hitOperationen
POST /graphql
Haupt-GraphQL-Endpoint. Hier alle GraphQL-Queries und -Mutationen mit c-id + Authorization-Headern senden. Der /peer_graphql-Endpoint ist das Peer-to-Peer-Äquivalent zwischen gekoppelten Geräten.
curl --request POST \
--url http://192.168.1.100:8080/graphql \
--header 'c-id: <client-id>' \
--header 'Authorization: Bearer <api-token>' \
--header 'Content-Type: application/json' \
--data '{"query":"{ app { battery deviceName } }"}'GET /fs
Liefert eine Datei, eine content://-URI oder ein Paket-Icon aus. Der id-Query-Parameter ist ein verschlüsselter String, der von files()/fileIds() zurückgegeben wird. Unterstützt Thumbnail-Erzeugung (?w=&h=&cc=), Byte-Bereiche (?offset=&length=) für Low-Throughput-Transporte, HEIF→PNG-Konvertierung, 3gp→MP4-Transcodierung und Download-Modus (dl=1) mit einem Content-Disposition-Attachment-Header.
# Original (full size, inline)
curl http://192.168.1.100:8080/fs?id=<encrypted-id> -o file.jpg
# Thumbnail (center-cropped to 200x200)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&w=200&h=200' -o thumb.jpg
# Download (attachment)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&dl=1' -OJ
# Byte range (BLE chunked download)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&offset=0&length=4096' \
-o chunk.binGET /proxyfs
Proxy für eine Peer-HTTP-URL. Der id-Query-Parameter entschlüsselt zu einer vollständigen http(s)-URL auf einem gekoppelten Peer-Gerät; der Server streamt die Upstream-Antwort zurück. Verwendet für Peer-to-Peer-Datei-Downloads über Wi-Fi Aware.
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpgPOST /upload
Upload einer einzelnen Datei via multipart/form-data. Der "info"-Teil (ChaCha20-verschlüsseltes JSON) muss vor dem "file"-Teil stehen. Wenn isAppFile=true, werden die Bytes in den inhaltsadressierbaren AppFileStore importiert (dedupliziert nach Hash); andernfalls wird die Datei nach info.dir/<fileName> geschrieben. Gibt den endgültigen Dateinamen zurück (kann ein "(1)"-Suffix haben, um Überschreiben zu vermeiden).
curl --request POST \
--url http://192.168.1.100:8080/upload \
--header 'c-id: <client-id>' \
--form 'info=<encrypted-info-bytes>;type=application/octet-stream' \
--form 'file=@/path/to/local.jpg'POST /upload_chunk
Upload eines Chunks eines fortsetzbaren, in Chunks aufgeteilten Uploads. Jeder Chunk wird auf der Festplatte unter upload_tmp/{fileId}/chunk_{index} gespeichert. Gibt "<index>:<savedSize>" zurück. Nachdem alle Chunks eingetroffen sind, die GraphQL-Mutation mergeChunks aufrufen, um die endgültige Datei zusammenzufügen.
curl --request POST \
--url http://192.168.1.100:8080/upload_chunk \
--header 'c-id: <client-id>' \
--form 'info=<encrypted-chunk-info-bytes>;type=application/octet-stream' \
--form 'file=@/path/to/chunk_0.bin'GET /zip/dir
Streamt ein einzelnes Verzeichnis als Zip-Archiv. Der id-Query-Parameter entschlüsselt zu einem Verzeichnispfad. Auf dem Gerät wird nur eine Zip-Operation gleichzeitig ausgeführt — gleichzeitige Anfragen erhalten HTTP 429.
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
-o folder.zipGET /zip/files
Streamt mehrere Dateien (oder Mediensuchergebnisse) als ein Zip. Der id entschlüsselt zu { type, query, id, name }. Für den Typ FILE wird die Dateiliste von der GraphQL files()-Query unter request.id in TempHelper gespeichert; für Medientypen führt der Server searchZipItems(type, query, id) aus.
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
-o selection.zipGET /media/{id}
DLNA-Medien-Endpoint. Liefert einen zuvor registrierten Medienpfad (registriert via UrlHelper.getMediaHttpUrl) an einen TV / DLNA-Renderer. URL-Quellen werden proxied, content://-URIs werden gestreamt, Bilder wie-ist geliefert und alle anderen Dateien mit DLNA-spezifischen Headern + HTTP 206-Range-Unterstützung, damit Renderer den Stream akzeptieren.
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4NOTIFY /callback/cast
DLNA-Renderer-Callback. Empfängt das NOTIFY-XML des Renderers und aktualisiert den CastPlayer-Zustand. Bei TransportState=STOPPED (ohne AVTransportURIMetaData) geht der Player automatisch zum nächsten Playlist-Eintrag über. Parst auch RelTime / TrackDuration für Positionsaktualisierungen.
# Sent by the DLNA renderer (not by the client):
NOTIFY /callback/cast HTTP/1.1
Content-Type: text/xml
<?xml ...><e:propertyset>...TransportState val="PLAYING"...</e:propertyset>GET /health
Unauthentifizierter Health-Check-Endpoint. Gibt den App-Paketnamen als Klartext zurück. Verwenden, um zu prüfen, ob der HTTP-Server erreichbar ist.
curl http://192.168.1.100:8080/healthGET /shutdown
HTTP-Server herunterfahren. Nur von localhost aufrufbar — Remote-Anfragen erhalten HTTP 403. Schließt alle WebSocket-Sessions, leert die Online-Client-Menge und disposed den HTTP-Server.
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdownPOST /init
Initialisiert eine Client-Session. Erfordert den c-id-Header. Wenn ein gültiger verschlüsselter Body vorhanden ist (mit dem zwischengespeicherten Token entschlüsselt), gilt die Session als authentifiziert und der Server antwortet 200. Andernfalls: wenn kein Passwort gesetzt ist, antwortet der Server mit einem frisch zurückgesetzten Passwort (200); wenn ein Passwort gesetzt ist, antwortet er 204 No Content und der Client muss sich über den WebSocket-Login-Flow authentifizieren.
curl --request POST \
--url http://192.168.1.100:8080/init \
--header 'c-id: <client-id>' \
--data-binary '<encrypted-token-bytes>'WS /
Haupt-WebSocket-Endpoint. ?cid=<client-id> verwenden, um eine Session zu registrieren; der erste binäre Frame muss ein verschlüsselter Timestamp sein (oder, mit ?auth=1, eine verschlüsselte AuthRequest mit dem Passwort). Nach der Authentifizierung pusht der Server verschlüsselte Ereignis-Frames (Benachrichtigungen, Chat-Updates etc.) über diesen Socket.
# Browser / JS example
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>')
ws.binaryType = 'arraybuffer'
ws.send(encryptWithToken(token, Date.now().toString()))
// Login variant:
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>&auth=1')
ws.send(encryptWithPassword(password, JSON.stringify({ password: hash })))WS /status
Peer-Presence-WebSocket. ?cid=<peer-id> verwenden, um einen gekoppelten Peer als online zu markieren. Der erste binäre Frame muss eine PeerChatParser-verschlüsselte Anfrage sein; bei Erfolg sendet der Server den Text "ok" und markiert den Peer als online, bis der Socket geschlossen wird.
# Peer-to-peer keepalive (typically opened automatically by the chat
# client after pairing — not usually invoked manually)
const ws = new WebSocket('ws://192.168.1.100:8080/status?cid=<peer-id>')
ws.send(peerEncryptedFrame)