Routes HTTP
PlainApp expose un petit ensemble d'endpoints HTTP à côté de l'API GraphQL à POST /graphql. Ces endpoints gèrent le service de fichiers, les uploads multipart, le streaming zip, le casting DLNA, les événements WebSocket et le contrôle système. Tous les endpoints de fichiers/upload/zip requièrent l'en-tête c-id (et généralement un paramètre de requête id chiffré).
Définitions de types
# 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 hitOpérations
POST /graphql
Endpoint GraphQL principal. Envoyez toutes les requêtes et mutations GraphQL ici avec les en-têtes c-id + Authorization. L'endpoint /peer_graphql est l'équivalent pair-à-pair utilisé entre les appareils appairés.
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
Sert un fichier, un URI content:// ou une icône de paquet. Le paramètre de requête id est une chaîne chiffrée renvoyée par files()/fileIds(). Prend en charge la génération de vignettes (?w=&h=&cc=), les plages d'octets (?offset=&length=) pour les transports à faible débit, la conversion HEIF→PNG, le transcodage 3gp→MP4 et le mode téléchargement (dl=1) avec un en-tête Content-Disposition attachment.
# 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 d'une URL HTTP pair. Le paramètre de requête id déchiffre une URL http(s) complète sur un appareil pair appairé ; le serveur renvoie la réponse amont en streaming. Utilisé pour les téléchargements de fichiers pair-à-pair via Wi-Fi Aware.
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpgPOST /upload
Upload d'un fichier unique via multipart/form-data. La partie « info » (JSON chiffré ChaCha20) doit précéder la partie « file ». Quand isAppFile=true, les octets sont importés dans l'AppFileStore à adressage par contenu (dédupliqué par hachage) ; sinon le fichier est écrit dans info.dir/<fileName>. Renvoie le nom final du fichier (peut avoir un suffixe « (1) » pour éviter l'écrasement).
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 d'un chunk d'un upload chunked reprendable. Chaque chunk est écrit sur disque dans upload_tmp/{fileId}/chunk_{index}. Renvoie « <index>:<savedSize> ». Après l'arrivée de tous les chunks, appelez la mutation GraphQL mergeChunks pour assembler le fichier final.
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
Stream un répertoire unique sous forme d'archive zip. Le paramètre de requête id déchiffre un chemin de répertoire. Une seule opération zip s'exécute à la fois sur l'appareil — les requêtes concurrentes reçoivent HTTP 429.
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
-o folder.zipGET /zip/files
Stream plusieurs fichiers (ou résultats de recherche média) sous forme de zip unique. Le id déchiffre { type, query, id, name }. Pour le type FILE la liste de fichiers est stockée dans TempHelper sous request.id par la requête GraphQL files() ; pour les types média le serveur exécute searchZipItems(type, query, id).
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
-o selection.zipGET /media/{id}
Endpoint média DLNA. Sert un chemin média préalablement enregistré (enregistré via UrlHelper.getMediaHttpUrl) à un téléviseur / renderer DLNA. Les sources URL sont proxyfiées, les URIs content:// sont streamés, les images sont servies telles quelles, et tous les autres fichiers sont servis avec des en-têtes spécifiques DLNA + support HTTP 206 range pour que les renderers acceptent le flux.
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4NOTIFY /callback/cast
Callback de renderer DLNA. Reçoit le XML NOTIFY de l'événement du renderer et met à jour l'état de CastPlayer. Sur TransportState=STOPPED (sans AVTransportURIMetaData) le player passe automatiquement à l'élément suivant de la liste de lecture. Analyse aussi RelTime / TrackDuration pour les mises à jour de position.
# 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
Endpoint de health-check non authentifié. Renvoie le nom du paquet de l'app en texte brut. Utilisez-le pour vérifier que le serveur HTTP est accessible.
curl http://192.168.1.100:8080/healthGET /shutdown
Arrête le serveur HTTP. Appelable uniquement depuis localhost — les requêtes distantes reçoivent HTTP 403. Ferme toutes les sessions WebSocket, vide l'ensemble des clients en ligne et dispose le serveur HTTP.
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdownPOST /init
Initialise une session client. Requiert l'en-tête c-id. Si un corps chiffré valide est présent (déchiffré avec le token en cache) la session est considérée comme authentifiée et le serveur répond 200. Sinon : si aucun mot de passe n'est défini, le serveur répond avec un mot de passe fraîchement réinitialisé (200) ; si un mot de passe est défini, répond 204 No Content et le client doit s'authentifier via le flux de connexion WebSocket.
curl --request POST \
--url http://192.168.1.100:8080/init \
--header 'c-id: <client-id>' \
--data-binary '<encrypted-token-bytes>'WS /
Endpoint WebSocket principal. Utilisez ?cid=<client-id> pour enregistrer une session ; la première trame binaire doit être un timestamp chiffré (ou, avec ?auth=1, une AuthRequest chiffrée contenant le mot de passe). Après authentification, le serveur pousse des trames d'événements chiffrées (notifications, mises à jour de chat, etc.) sur ce 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
WebSocket de présence pair. Utilisez ?cid=<peer-id> pour garder un pair appairé marqué en ligne. La première trame binaire doit être une requête chiffrée PeerChatParser ; en cas de succès le serveur envoie le texte « ok » et marque le pair en ligne jusqu'à la fermeture du socket.
# 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)