HTTP Routes
PlainApp cung cấp một tập hợp nhỏ các HTTP endpoint bên cạnh GraphQL API tại POST /graphql. Các endpoint này xử lý phục vụ tệp, tải lên multipart, streaming zip, DLNA casting, sự kiện WebSocket và điều khiển hệ thống. Tất cả endpoint file/upload/zip yêu cầu header c-id (và thường là tham số truy vấn id được mã hóa).
Định nghĩa kiểu
# 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 hitThao tác
POST /graphql
GraphQL endpoint chính. Gửi tất cả truy vấn và mutation GraphQL tới đây với header c-id + Authorization. Endpoint /peer_graphql là tương đương peer-to-peer được sử dụng giữa các thiết bị đã ghép nối.
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
Phục vụ một tệp, content:// URI, hoặc biểu tượng gói. Tham số truy vấn id là một chuỗi được mã hóa trả về bởi files()/fileIds(). Hỗ trợ tạo thumbnail (?w=&h=&cc=), byte-range (?offset=&length=) cho transport thông lượng thấp, chuyển đổi HEIF→PNG, transcode 3gp→MP4, và chế độ tải xuống (dl=1) với header 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 một peer HTTP URL. Tham số truy vấn id giải mã thành một http(s) URL đầy đủ trên thiết bị peer đã ghép nối; máy chủ stream phản hồi upstream trở lại. Được sử dụng cho tải xuống tệp peer-to-peer qua Wi-Fi Aware.
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpgPOST /upload
Tải lên một tệp duy nhất qua multipart/form-data. Phần "info" (JSON mã hóa ChaCha20) phải đứng trước phần "file". Khi isAppFile=true, các byte được nhập vào AppFileStore content-addressable (khử trùng lặp theo hash); nếu không, tệp được ghi vào info.dir/<fileName>. Trả về tên tệp cuối cùng (có thể có hậu tố "(1)" khi tránh ghi đè).
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
Tải lên một chunk của quá trình tải lên có thể tiếp tục, theo chunk. Mỗi chunk được ghi vào upload_tmp/{fileId}/chunk_{index} trên đĩa. Trả về "<index>:<savedSize>". Sau khi tất cả chunk đến, gọi mutation mergeChunks GraphQL để lắp ráp tệp cuối cùng.
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 một thư mục duy nhất dưới dạng zip archive. Tham số truy vấn id giải mã thành một đường dẫn thư mục. Chỉ một thao tác zip chạy tại một thời điểm trên thiết bị — các yêu cầu đồng thời nhận HTTP 429.
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
-o folder.zipGET /zip/files
Stream nhiều tệp (hoặc kết quả tìm kiếm media) dưới dạng một zip duy nhất. id giải mã thành { type, query, id, name }. Cho loại FILE, danh sách tệp được lưu trong TempHelper dưới request.id bởi truy vấn files() GraphQL; cho các loại media, máy chủ chạy searchZipItems(type, query, id).
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
-o selection.zipGET /media/{id}
DLNA media endpoint. Phục vụ một đường dẫn media đã đăng ký (đăng ký qua UrlHelper.getMediaHttpUrl) cho một TV / DLNA renderer. Nguồn URL được proxy, content:// URI được stream, hình ảnh được phục vụ nguyên dạng, và tất cả tệp khác được phục vụ với header DLNA-specific + hỗ trợ HTTP 206 range để renderer chấp nhận stream.
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4NOTIFY /callback/cast
DLNA renderer callback. Nhận NOTIFY XML sự kiện của renderer và cập nhật trạng thái CastPlayer. Khi TransportState=STOPPED (không có AVTransportURIMetaData), player tự động chuyển sang mục tiếp theo trong playlist. Cũng parse RelTime / TrackDuration cho cập nhật vị trí.
# 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 health-check không yêu cầu xác thực. Trả về tên gói ứng dụng dưới dạng plain text. Sử dụng để xác minh máy chủ HTTP có thể truy cập được.
curl http://192.168.1.100:8080/healthGET /shutdown
Tắt máy chủ HTTP. Chỉ có thể gọi từ localhost — yêu cầu từ xa nhận HTTP 403. Đóng tất cả phiên WebSocket, xóa tập hợp client trực tuyến, và dispose máy chủ HTTP.
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdownPOST /init
Khởi tạo một phiên client. Yêu cầu header c-id. Nếu có body mã hóa hợp lệ (giải mã bằng token đã cache), phiên được coi là đã xác thực và máy chủ phản hồi 200. Nếu không: nếu chưa đặt mật khẩu, máy chủ phản hồi với mật khẩu mới đặt lại (200); nếu đã đặt mật khẩu, phản hồi 204 No Content và client phải xác thực qua flow đăng nhập WebSocket.
curl --request POST \
--url http://192.168.1.100:8080/init \
--header 'c-id: <client-id>' \
--data-binary '<encrypted-token-bytes>'WS /
WebSocket endpoint chính. Sử dụng ?cid=<client-id> để đăng ký phiên; khung binary đầu tiên phải là một timestamp mã hóa (hoặc, với ?auth=1, một AuthRequest mã hóa chứa mật khẩu). Sau khi xác thực, máy chủ đẩy các khung sự kiện mã hóa (thông báo, cập nhật chat, v.v.) qua socket này.
# 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 peer-presence. Sử dụng ?cid=<peer-id> để giữ một peer đã ghép nối được đánh dấu trực tuyến. Khung binary đầu tiên phải là một yêu cầu mã hóa PeerChatParser; khi thành công, máy chủ gửi văn bản "ok" và đánh dấu peer trực tuyến cho đến khi socket đóng.
# 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)