Rotas HTTP
PlainApp expõe um pequeno conjunto de endpoints HTTP junto à API GraphQL em POST /graphql. Esses endpoints tratam serviço de arquivos, uploads multipart, streaming zip, casting DLNA, eventos WebSocket e controle do sistema. Todos os endpoints de arquivo/upload/zip exigem o cabeçalho c-id (e geralmente um parâmetro de consulta id criptografado).
Definições de Tipo
# 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 hitOperações
POST /graphql
Endpoint GraphQL principal. Envie todas as queries e mutations GraphQL aqui com os cabeçalhos c-id + Authorization. O endpoint /peer_graphql é o equivalente peer-to-peer usado entre dispositivos pareados.
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
Serve um arquivo, URI content:// ou ícone de pacote. O parâmetro de consulta id é uma string criptografada retornada por files()/fileIds(). Suporta geração de miniaturas (?w=&h=&cc=), byte-range (?offset=&length=) para transportes de baixa taxa, conversão HEIF→PNG, transcodificação 3gp→MP4 e modo download (dl=1) com um cabeçalho Content-Disposition do tipo 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 de uma URL HTTP de par. O parâmetro de consulta id descriptografa para uma URL http(s) completa em um dispositivo par pareado; o servidor transmite a resposta upstream de volta. Usado para downloads de arquivos peer-to-peer via Wi-Fi Aware.
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpgPOST /upload
Faz upload de um único arquivo via multipart/form-data. A parte "info" (JSON criptografado com ChaCha20) deve preceder a parte "file". Quando isAppFile=true os bytes são importados para o AppFileStore content-addressable (deduplicado por hash); caso contrário o arquivo é gravado em info.dir/<fileName>. Retorna o nome final do arquivo (pode ter um sufixo "(1)" ao evitar sobrescrita).
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
Faz upload de um chunk de um upload em partes e retomável. Cada chunk é gravado em upload_tmp/{fileId}/chunk_{index} no disco. Retorna "<index>:<savedSize>". Após todos os chunks chegarem, chame a mutation GraphQL mergeChunks para montar o arquivo 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
Transmite uma única pasta como um arquivo zip. O parâmetro de consulta id descriptografa para um caminho de pasta. Apenas uma operação zip é executada por vez no dispositivo — requisições concorrentes recebem HTTP 429.
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
-o folder.zipGET /zip/files
Transmite vários arquivos (ou resultados de busca de mídia) como um único zip. O id descriptografa para { type, query, id, name }. Para o tipo FILE a lista de arquivos é armazenada em TempHelper sob request.id pela query GraphQL files(); para tipos de mídia o servidor executa searchZipItems(type, query, id).
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
-o selection.zipGET /media/{id}
Endpoint de mídia DLNA. Serve um caminho de mídia registrado anteriormente (registrado via UrlHelper.getMediaHttpUrl) a uma TV / renderer DLNA. Fontes de URL são feitas proxy, URIs content:// são transmitidos, imagens servidas como estão e todos os outros arquivos são servidos com cabeçalhos específicos de DLNA + suporte a HTTP 206 range para que os renderers aceitem o stream.
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4NOTIFY /callback/cast
Callback do renderer DLNA. Recebe o XML NOTIFY dos eventos do renderer e atualiza o estado do CastPlayer. Em TransportState=STOPPED (sem AVTransportURIMetaData) o player avança automaticamente para o próximo item da playlist. Também analisa RelTime / TrackDuration para atualizações de posição.
# 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 não autenticado. Retorna o nome do pacote do app como texto puro. Use-o para verificar se o servidor HTTP está acessível.
curl http://192.168.1.100:8080/healthGET /shutdown
Desliga o servidor HTTP. Invocável apenas de localhost — requisições remotas recebem HTTP 403. Fecha todas as sessões WebSocket, limpa o conjunto de clientes online e descarta o servidor HTTP.
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdownPOST /init
Inicializa uma sessão de cliente. Requer o cabeçalho c-id. Se um corpo criptografado válido estiver presente (descriptografado com o token em cache) a sessão é considerada autenticada e o servidor responde 200. Caso contrário: se nenhuma senha estiver definida, o servidor responde com uma senha recém-redefinida (200); se uma senha estiver definida, responde 204 No Content e o cliente deve autenticar via fluxo de login 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. Use ?cid=<client-id> para registrar uma sessão; o primeiro frame binário deve ser um timestamp criptografado (ou, com ?auth=1, uma AuthRequest criptografada contendo a senha). Após a autenticação o servidor envia frames de eventos criptografados (notificações, atualizações de chat, etc.) por este 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 presença de par. Use ?cid=<peer-id> para manter um par pareado marcado como online. O primeiro frame binário deve ser uma requisição criptografada por PeerChatParser; em caso de sucesso o servidor envia o texto "ok" e marca o par como online até que o socket seja fechado.
# 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)