restapi: full CLI parity + production readiness - #817
Conversation
|
Hi @danielpaulus , @shamanec , I couldn't find a better place to ask the question so I write here as comment to the latest pull request :). Do you have any plan for the next release date even roughly? Thanks. |
…ts, no-panic marshal) Foundational hardening for running the REST API in production. No endpoint behavior changes; the auth model from #792 (GO_IOS_API_KEY / --disable-auth) is preserved. - Serve via an explicit http.Server with graceful shutdown on SIGINT/SIGTERM (drains in-flight requests, 10s timeout). - Bound abuse without breaking streams: ReadHeaderTimeout, IdleTimeout, MaxHeaderBytes. Deliberately no WriteTimeout so /syslog,/listen,/ostrace, /notifications can stream indefinitely. - Optional TLS via --tls-cert/--tls-key; configurable bind via --addr (default :8080). Flags parsed in the same tolerant FlagSet as --disable-auth. - Unauthenticated /healthz and /readyz probes outside /api/v1. - Gate the swagger UI behind auth (served under /api/v1) when a token is set. - MustMarshal no longer panics on an unmarshalable value — it returns a JSON error envelope, so a stream/handler can't be crashed by it. - Add RespondError helper for a consistent {"error":...} envelope. Tests: MustMarshal no-panic + valid-unchanged, health endpoints, RespondError, parseServerConfig (defaults, flags, tolerates unknown args). go build/vet/test ./restapi/... green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…s, mobilegestalt, ps, lockdown)
First batch of CLI-parity endpoints, all read-only, each mirroring the exact
go-ios library call the corresponding `ios` CLI command uses:
- GET /device/:udid/devicename -> ios.GetValues (ios devicename)
- GET /device/:udid/date -> ios.GetValues (ios date)
- GET /device/:udid/battery -> ios.GetBatteryDiagnostics (ios batterycheck)
- GET /device/:udid/diagnostics -> diagnostics.AllValues (ios diagnostics list)
- GET /device/:udid/mobilegestalt -> diagnostics.MobileGestaltQuery (ios mobilegestalt), keys via ?key=
- GET /device/:udid/processes -> instruments.ProcessList (ios ps), ?apps=true filters apps
- GET /device/:udid/lockdown -> ios.GetValues (ios lockdown get)
Handlers surface library errors as {"error":...} via RespondError (no discarded
errors, no panics). Wired through registerDeviceInfoRoutes in routes.go.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…e, lang, memlimitoff) Mirrors the ios CLI: diagnostics.Reboot/Shutdown, mcinstall.Erase (gated by ?confirm=true), amfi.EnableDeveloperMode + imagemounter.IsDevModeEnabled + amfi.RevealDevMode, ios.Get/SetLanguage, and instruments ProcessControl DisableMemoryLimit. Errors surfaced via RespondError; no panics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…h, ios crash ls/rm) Uses the iOS 17+ file service and streams pull/push through the HTTP body, so there is no caller-supplied host path and no host-side traversal. Domains: app|app-group|crash|temp. Crash reports: list + remove (crash-log downloads are available via /files?domain=crash). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
- GET/PUT /wallpaper: springboard GetHomeScreenWallpaperPNG (image/png); set via multipart (image+p12 supervisor identity+screen) -> mcinstall.SetWallpaperSupervised. - GET/PUT /icon-layout: springboard Get/SetIconLayout. - GET/PUT /pasteboard: pasteboard Get/SetText (iOS 17+). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
…age list/unmount) - POST /profiles: mcinstall AddProfile / AddProfileSupervised (multipart p12); DELETE /profiles/:name. - GET /image/list: mounted image signatures (hex); DELETE /image: imagemounter.UnmountImage. GET /profiles and GET/PUT /image already existed; only the missing verbs added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
- GET/PUT /assistivetouch: ios.Get/SetAssistiveTouch. - GET/PUT /timeformat: ios.Get/SetUses24HourClock. - PUT/DELETE /wifi: mcinstall.PrepareWifi/RemoveWifi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
GET /sysmontap streams instruments Sysmontap CPU-usage samples (matches the existing syslog/listen streaming pattern). pcap is deferred until ios/pcap exposes a packet-callback streaming API (it currently writes a local file).
…, clear-passcode, clear-screen-time-password) All under /device/:udid/mdm, POST multipart with a p12 supervisor identity + password (escalated mcinstall session via conn.Escalate). Credentials stay in memory, never logged or persisted. clear-passcode also takes a base64 token.
…streamed logs Long-running device operations now run as background jobs: - POST /jobs/runtest, /jobs/runwda, /jobs/forward -> 202 + job id - GET /jobs (per-device), GET /jobs/:id (status), DELETE /jobs/:id (stop) - GET /jobs/:id/logs streams that job's isolated log (history + live tail) Each job captures its output on a dedicated jobLog sink (io.Writer wired into the testmanagerd TestListener), so concurrent jobs never interleave. Lifecycle events are logged via ios/golog with module=go-ios/restapi + udid + job attrs. Terminal state is immutable, so stopping a job isn't relabeled as a failure when its context-cancelled goroutine returns. In-memory job manager + jobLog are unit-tested (incl. -race): lifecycle, stop-is-terminal, per-device isolation, log stream/close. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJXoR6KAaEXEqzyJien7WJ
ea8ae66 to
6e88833
Compare
PUT /httpproxy (supervised, multipart host/port/p12/user/pass/password) -> mcinstall.SetHttpProxy; DELETE /httpproxy -> mcinstall.RemoveProxy.
Agent-level, not device-scoped, so they live at /api/v1 (behind auth): - GET /tunnels list running tunnels - DELETE /tunnels/:udid stop a device tunnel - POST /tunnels/:udid/refresh refresh a device tunnel - POST /tunnel-agent/shutdown stop the tunnel agent They query the running agent via ios.HttpApiHost/HttpApiPort. 'tunnel start' is not exposed (privileged long-running daemon). Added a route-registration smoke test that builds the full tree so gin route conflicts fail loudly.
httptest-based tests exercising the request-validation branches (which run before any device I/O): missing/invalid params across files, mobilegestalt, wifi, mdm, crashes, devmode, and the job endpoints (erase confirm-gate, missing bundle/ports, job-not-found 404). Closes the biggest coverage gap for the parity endpoints.
e5b3cc5 to
a0ee58d
Compare
Add RateLimitUDID: a token-bucket (golang.org/x/time/rate) gin middleware keyed by device UDID, applied across the /device/:udid group. Requests over the limit get 429; each UDID has its own bucket so devices don't throttle each other. Configurable via --rate-limit (req/s, default 20) and --rate-burst (default 40); 0 disables. Uses the atomic sync.Map LoadOrStore pattern (no create race). Tests (device-free, -race): burst-then-429, disabled-when-zero, per-device isolation, and a concurrent-hammer test asserting the shared bucket isn't exceeded under load.
Adversarial review follow-ups on the 56-endpoint REST parity work: Security/correctness fixes: - Bound in-memory uploads: readFormFile and the raw-body reads in SetPasteboard and AddProfile now go through readAllLimited (256 MiB cap) so an authenticated client can't OOM the daemon with an oversized multipart file or body. PushFile already streams (Content-Length gated) and is unaffected. - Fix a lost-line gap in GET /jobs/:id/logs: snapshot() then subscribe() raced, dropping any line written in between. Added jobLog. snapshotAndSubscribe() which takes the backlog and the live subscription under one lock. - Bound the process-wide job registry: DELETE /jobs/:id on an already terminal job now purges it (jobManager.remove, terminal-only) so finished jobs' buffered logs don't accumulate forever. Running jobs are still stopped, never silently dropped. Tests (httptest + in-context device): - Auth coverage: BearerAuth accept/reject, and a tree-walk asserting all 80 registered /api/v1 routes return 401 unauthenticated. - Files: Content-Disposition base-name sanitisation (traversal-y remote can't inject a host path), ls/pull/push validation, push 411 without Content-Length. - Upload limits: readAllLimited boundaries + oversized pasteboard body. - Proxy/MDM multipart validation. - Jobs: full HTTP lifecycle (create/list/get/stop/delete), per-device isolation (cross-udid GET/DELETE 404 and no stop), remove-terminal-only, atomic snapshot+subscribe. go build/vet/test ./restapi/... green incl. -race; gofmt clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
Review round: adversarial security + correctness pass (pushed cbde822)Reviewed all 56 endpoints for the four priority classes (files traversal, proxy SSRF, auth gaps, jobs races/resource exhaustion), got a second opinion from Codex, verified each claim, and fixed the confirmed defects with tests. CI triage
Confirmed defects fixed
Reviewed and dismissed (not defects)
Streaming framing inventory (for the separate SSE workstream — not changed here)
Both new streaming endpoints are already newline-delimited. The two undelimited legacy streams ( Tests added (
|
Make the REST daemon match the TypeSpec-authored OpenAPI contract the
official SDKs are generated from.
Real SSE framing (event:/data:/blank-line) on all six streaming
endpoints, with a periodic heartbeat on idle, replacing the previous
NDJSON/concatenated-JSON writes:
- /syslog -> event "syslog" (SyslogMessage)
- /notifications-> event "appstate" (AppStateNotification)
- /ostrace -> event "ostrace" (OsTraceEntry)
- /listen -> event "attachdetach" (AttachDetachEvent)
- /sysmontap -> event "sample" (CpuUsageSample)
- /jobs/{id}/logs-> event "log" (JobLogLine)
- all -> event "heartbeat" ({}) on idle
Payload models use the spec's camelCase field names; a shared streamSSE
helper drives frames + heartbeats and flushes after each write.
Other spec conformance:
- setlocation: longtitude -> longitude (query param, checks, messages,
swagger annotations)
- screenshot: content-type image/png (was application/octet-stream)
- streaming error paths now use the GenericResponse envelope
Device-free unit tests cover the SSE framing + heartbeats, payload
mappers, longitude, and the removed misspelled param.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
SDK-contract conformance passMade the REST daemon conform to the TypeSpec-authored OpenAPI contract the SDKs are generated from ( Real SSE framing on all six streaming endpointsReplaced the previous NDJSON / concatenated-JSON writes with proper SSE frames (
Payload structs use the spec's camelCase field names. Other spec fixes
Tests (device-free)
Divergences deliberately left
|
Integrate the five per-language examples suites (typescript, python, java, csharp, mcp) and tie them together: - sdks/docs/EXAMPLES.md: top-level overview (docs + pre-release smoke test), per-language index, shared GO_IOS_BASE_URL/GO_IOS_API_KEY/GO_IOS_UDID/RUN_UI convention, how to run them all. Linked from sdks/README.md. Documents the canonical daemon port (8080, matching the daemon's --addr default) and the SDK-library-vs-daemon default discrepancy (libraries default to :60105 from the spec servers URL) for a follow-up reconcile. - sdks.yml: add the device-free MCP list-tools smoke check (npm run examples) to the mcp job so a broken MCP server fails CI. - verify-sdk-examples.yml: new dispatch-only, farm-gated workflow that runs every SDK's example runner against a live go-ios REST daemon + device. Inert until the full REST daemon lands on main (#817/#821); the daemon-start step is a documented placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the REST API to feature parity with the
iosCLI and makes it production-grade. Auth model from #792 (GO_IOS_API_KEY/--disable-auth) is preserved. 56 new routes across 12 commits;go build/vet/test ./restapi/...green (incl.-race).Production readiness
http.Serverwith graceful shutdown (SIGINT/SIGTERM), stream-safe timeouts (ReadHeaderTimeout/IdleTimeout/MaxHeaderBytes, deliberately noWriteTimeoutso streaming endpoints survive), optional TLS (--tls-cert/--tls-key), configurable bind (--addr)./healthz+/readyz; swagger UI gated behind auth when a token is set.MustMarshalno longer panics (error envelope);RespondErrorhelper for a consistent{"error":...}shape.ios/golog-based structured logging (module=go-ios/restapi+udid+jobattrs).Endpoints (mirroring the exact library call each CLI command uses)
/api/v1): GET /tunnels, DELETE /tunnels/:udid, POST /tunnels/:udid/refresh, POST /tunnel-agent/shutdownTests
-race.Remaining / intentionally out
ui(tap/swipe/button/type/app/…): the CLI'suiis an HTTP client proxying to WDA (:8100) / DeviceKit (:12004), not a go-ios library — see the open design note below. In the meantime UI automation is reachable through the API viaPOST /jobs/runwda+POST /jobs/forward(host→8100) then talking to WDA directly.runxctest(xctestrun-file upload) — planned follow-up on the job subsystem.pcap— deferred untilios/pcapexposes a packet-callback streaming API (it currently writes a local file).debug(lldb) anddproxy;tunnel start(privileged daemon);sign/prepare create-cert(host-local, no device).Follow-ups before merge
🤖 Generated with Claude Code