Commit graph

8 commits

Author SHA1 Message Date
ScreenTinker dec56506f9 i18n: add Android localized string resources
Adds values-{es,fr,de,pt,hi}/strings.xml mirroring values/strings.xml.
Two strings: app_name (kept as RemoteDisplay across all locales) and
the accessibility service description (translated).

Hindi is a copy of English by design — same approach as the web's
empty hi.js. Native review can replace the en text in place once
done; Android picks the right file based on device language.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:20:14 -05:00
ScreenTinker 66a137cffe Android: bump to 1.7.8 + fix safeOn return type
Released APK 1.7.8 includes the OOM/crash-loop fix, WebSocket crash
hardening, and the http(s)-only ImageLoader scheme guard. Bumped
versionCode 10 -> 11 and versionName 1.7.7 -> 1.7.8 so existing
1.7.7 installs auto-update on the next UpdateChecker poll.

Also fixed the safeOn extension function: Socket.on() returns Emitter,
not Socket, so the original `return on(...)` failed compile with a
type mismatch. Switched to `on(...); return this` for proper chaining.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:45:18 -05:00
ScreenTinker c105a5941e Security: fix IDORs, XSS, rate limits, SSRF validation
HIGH 1 (teams IDOR): POST/DELETE /api/teams/:id/devices now require the
caller to own the device before assigning or detaching it. Without this
check, any team member could pull any device into their team via UUID
guess and gain remote-control access.

HIGH 2 (schedules IDOR): PUT /api/schedules/:id now re-verifies
ownership of every changed target field — device_id, group_id,
content_id, widget_id, layout_id, playlist_id. Previously only the
schedule owner was checked, letting users fire arbitrary content on
victim devices via update.

HIGH 3 (filename XSS): file.originalname captured by multer bypassed
sanitizeBody. New safeFilename() wraps every INSERT path (multipart
upload, remote URL, YouTube). Frontend sinks now go through esc() in
content-library.js, device-detail.js, video-wall.js. Web player gets
an inline escHtml helper for its info overlay where filenames, device
name, and serverUrl land in innerHTML.

HIGH 4 (kiosk public XSS): config.idleTimeout is now coerced via the
existing safeNumber() helper at both interpolation sites. A crafted
value with a newline can no longer escape the JS line comment to
inject arbitrary code into the public render endpoint.

HIGH 5 (folder DoS): POST /api/folders enforces a per-user cap of 100
folders (429 on overflow). Superadmin exempt.

MED 1 (SSRF): ImageLoader.decodeUrl rejects any URL scheme other than
http(s) so a malicious remote_url can't read local files via file://.
On the server, validateRemoteUrl() is extracted and now also runs on
PUT /api/content/:id remote_url updates — previously the SSRF check
only fired on POST.

MED 2 (fingerprint takeover): the WS device:register fingerprint
reclaim path now rejects takeover while the target device is online or
within 24h of its last heartbeat. A leaked fingerprint can no longer
hijack an active display.

MED 3 (npm audit): bumped uuid 9.x -> 14.0.0 (v3/v5/v6 buffer bounds
CVE; we only use v4 so not exploitable, but clears the audit). path-
to-regexp resolved to 0.1.13 via npm audit fix. 0 vulns remaining.

MED 4 (folder admin consistency): ownedFolder() and the content.js
folder_id move check now both treat only superadmin as privileged,
matching GET /api/folders. Previously a plain "admin" could rename
or delete folders they couldn't see, and could move content into
folders they couldn't list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:37:18 -05:00
ScreenTinker 8866e305f0 Fix Android app crash on WebSocket connection loss
Every Socket.IO listener now goes through a safeOn helper that wraps
the body in try/catch(Throwable). Unsafe args[0] as JSONObject and
data.getString() patterns replaced with firstOrNull as? JSONObject
and optString — a malformed payload from the server, or a transient
state error during disconnect, no longer surfaces as an unhandled
exception on the IO thread.

Reconnection now uses explicit exponential backoff with jitter
(1s → 60s, randomizationFactor 0.5) so a fleet doesn't reconnect in
lockstep after a server blip. EVENT_DISCONNECT stops the heartbeat
while disconnected; the player keeps showing cached content. register,
sendHeartbeat, requestPlaylistRefresh, sendScreenshot, sendContentAck,
sendPlaybackState, and disconnect are all wrapped — telemetry / WiFi
service calls can throw on some devices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:13:26 -05:00
ScreenTinker cd6e39a4a7 Fix Android app OOM crash on 4K images and crash loop recovery
A 4K image assigned to a 1080p display decoded as a ~33 MB ARGB_8888
bitmap and OOM'd. Worse, the cached playlist on disk meant relaunch
hit the same image and crashed again — only a reinstall recovered.

New ImageLoader utility reads bounds via inJustDecodeBounds, computes
inSampleSize against the device screen (or zone size for multi-zone
layouts), and returns null on OOM/Throwable so callers skip the item
instead of crashing. MediaPlayerManager exposes an onImageError
callback wired to playlistController.next() so a bad item advances
the playlist. The cached-playlist restore in onCreate now catches
Throwable (was Exception) and clears the cache on any failure,
breaking the crash loop. android:largeHeap="true" added as belt and
braces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 10:13:10 -05:00
ScreenTinker dc7450b6a7 Offline resilience: persist playlist cache for cold-start recovery
Web player:
- Cache playlist JSON to localStorage on every update
- Restore and start playing immediately on boot before connecting
- Clear cache on unpair/reset

Android app:
- Cache playlist JSON to EncryptedSharedPreferences on every update
- Restore cached playlist on cold-start, play from disk-cached content
- Update cache on content deletion, clear on unpair

Server (device socket):
- Fingerprint reconnect: issue fresh token instead of rejecting
- Send device:paired on fingerprint recovery for claimed devices
- Add status logging and dashboard notification on fingerprint reconnect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:49:45 -05:00
ScreenTinker 1d253c4cae Android + web player: handle device_token authentication
Follows up on the security audit remediation (afbe113) which added
device_token auth to the WebSocket /device namespace.

Android player (ServerConfig.kt, WebSocketService.kt):
- Persist device_token in EncryptedSharedPreferences alongside device_id
- Send device_token in device:register on reconnect and playlist refresh
- Save/overwrite token from device:registered response (handles legacy
  devices getting their first token)
- Handle device:auth-error by clearing credentials and showing pairing screen
- clearDeviceCredentials() method wipes device_id, device_token, is_paired

Web player (player/index.html):
- Save deviceToken in localStorage config from device:registered response
- Send device_token in register() payload on reconnect
- Handle device:auth-error and device:unpaired events — clear config and
  show re-pair UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 22:52:52 -05:00
ScreenTinker 1594a9d4a4 Initial open source release
ScreenTinker - open source digital signage management software.
MIT License, all features included, no license gates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 12:14:53 -05:00