HTTP 라우트
PlainApp는 POST /graphql의 GraphQL API와 함께 소규모 HTTP 엔드포인트를 노출합니다. 이 엔드포인트들은 파일 제공, 멀티파트 업로드, zip 스트리밍, DLNA 캐스팅, WebSocket 이벤트, 시스템 제어를 처리합니다. 모든 file/upload/zip 엔드포인트에는 c-id 헤더 (일반적으로 암호화된 id 쿼리 매개변수도)가 필요합니다.
타입 정의
# 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 hit작업
POST /graphql
메인 GraphQL 엔드포인트. 모든 GraphQL 쿼리와 뮤테이션을 c-id + Authorization 헤더와 함께 여기로 보냅니다. /peer_graphql 엔드포인트는 페어링된 기기 간에 사용되는 피어 투 피어 동등한 엔드포인트입니다.
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
파일, content:// URI 또는 패키지 아이콘을 제공합니다. id 쿼리 매개변수는 files()/fileIds()에서 반환되는 암호화된 문자열입니다. 썸네일 생성 (?w=&h=&cc=), 저처량 전송용 바이트 범위 (?offset=&length=), HEIF→PNG 변환, 3gp→MP4 트랜스코딩, Content-Disposition 첨부 헤더가 있는 다운로드 모드 (dl=1)를 지원합니다.
# 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
peer HTTP URL을 프록시합니다. id 쿼리 매개변수는 페어링된 peer 기기의 전체 http(s) URL로 복호화됩니다. 서버는 업스트림 응답을 스트리밍하여 반환합니다. Wi-Fi Aware를 통한 피어 투 피어 파일 다운로드에 사용됩니다.
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpgPOST /upload
multipart/form-data로 단일 파일을 업로드합니다. "info" 부분 (ChaCha20 암호화 JSON)이 "file" 부분보다 먼저 와야 합니다. isAppFile=true인 경우 바이트는 콘텐츠 주소 지정 가능한 AppFileStore로 가져와지고 (해시로 중복 제거), 그렇지 않으면 파일이 info.dir/<fileName>에 기록됩니다. 최종 파일 이름을 반환합니다 (덮어쓰기 방지 시 "(1)" 접미사가 붙을 수 있음).
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_tmp/{fileId}/chunk_{index}에 기록됩니다. "<index>:<savedSize>"를 반환합니다. 모든 청크가 도착한 후 mergeChunks GraphQL 뮤테이션을 호출하여 최종 파일을 조립합니다.
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
단일 디렉터리를 zip 아카이브로 스트리밍합니다. id 쿼리 매개변수는 디렉터리 경로로 복호화됩니다. 기기에서 동시에 하나의 zip 작업만 실행됩니다 — 동시 요청은 HTTP 429를 받습니다.
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
-o folder.zipGET /zip/files
여러 파일 (또는 미디어 검색 결과)을 단일 zip으로 스트리밍합니다. id는 { type, query, id, name }으로 복호화됩니다. FILE 타입의 경우 파일 목록은 GraphQL files() 쿼리에 의해 TempHelper의 request.id 아래에 저장됩니다. 미디어 타입의 경우 서버는 searchZipItems(type, query, id)를 실행합니다.
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
-o selection.zipGET /media/{id}
DLNA 미디어 엔드포인트. 이전에 등록된 미디어 경로 (UrlHelper.getMediaHttpUrl로 등록)를 TV / DLNA 렌더러에 제공합니다. URL 소스는 프록시되고, content:// URI는 스트리밍되며, 이미지는 그대로 제공되고, 다른 모든 파일은 DLNA 특정 헤더 + HTTP 206 범위 지원과 함께 제공되어 렌더러가 스트림을 수락할 수 있습니다.
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4NOTIFY /callback/cast
DLNA 렌더러 콜백. 렌더러의 이벤트 NOTIFY XML을 수신하고 CastPlayer 상태를 업데이트합니다. TransportState=STOPPED (AVTransportURIMetaData 없음) 시 플레이어가 재생 목록의 다음 항목으로 자동 진행됩니다. 위치 업데이트를 위해 RelTime / TrackDuration도 파싱합니다.
# 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
인증 불필요 헬스 체크 엔드포인트. 앱 패키지 이름을 일반 텍스트로 반환합니다. HTTP 서버가 연결 가능한지 확인하는 데 사용합니다.
curl http://192.168.1.100:8080/healthGET /shutdown
HTTP 서버를 종료합니다. localhost에서만 호출 가능 — 원격 요청은 HTTP 403을 받습니다. 모든 WebSocket 세션을 닫고, 온라인 클라이언트 세트를 지우고, HTTP 서버를 폐기합니다.
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdownPOST /init
클라이언트 세션을 초기화합니다. c-id 헤더가 필요합니다. 유효한 암호화된 본문 (캐시된 token으로 복호화)이 있으면 세션은 인증된 것으로 간주되고 서버는 200으로 응답합니다. 그렇지 않은 경우: 비밀번호가 설정되지 않은 경우 서버는 새로 재설정된 비밀번호로 응답하고 (200), 비밀번호가 설정된 경우 204 No Content로 응답하며 클라이언트는 WebSocket 로그인 흐름을 통해 인증해야 합니다.
curl --request POST \
--url http://192.168.1.100:8080/init \
--header 'c-id: <client-id>' \
--data-binary '<encrypted-token-bytes>'WS /
메인 WebSocket 엔드포인트. ?cid=<client-id>를 사용하여 세션을 등록합니다. 첫 번째 바이너리 프레임은 암호화된 타임스탬프 (또는 ?auth=1인 경우 비밀번호를 포함한 암호화된 AuthRequest)여야 합니다. 인증 후 서버는 이 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 프레즌스 WebSocket. ?cid=<peer-id>를 사용하여 페어링된 peer를 온라인으로 유지합니다. 첫 번째 바이너리 프레임은 PeerChatParser 암호화 요청이어야 합니다. 성공 시 서버는 "ok" 텍스트를 보내고 socket이 닫힐 때까지 peer를 온라인으로 표시합니다.
# 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)