a1e3e6852f
Resilience + scale pass on top of the v1 core. Stream health prober: - New SSRF-validated /probe endpoint (HEAD -> ranged-GET fallback, 6s timeout, 5-min TTL cache, concurrency-capped, no-store) + orb_probe_total metrics - Front-end health.js: lazy bounded-concurrency probing (current + sampled channels, not the whole catalog), LIVE/DEGRADED/DEAD scoring, guide skull glyph, instant no-signal on zap to a known-dead channel Oracle's Memory (memory.js): - Session resurrection (last channel, volume, mute, theater/dim/ambient) - Exponential-decay per-channel uptime history; pinning; dead-channel auto-skip Off-thread XMLTV: - lib/xmltv-parse.js (pure, hand-rolled scanner, no DOMParser/regex backtracking) run in epg-worker.js module worker; wired into the iptv-org provider - Unit-tested in node: entity decode, TZ offsets, 20k programmes off-thread Service worker / PWA: - manifest.webmanifest + orb icons; sw.js (cache-first shell + hls.js, stale-while-revalidate for JSON-over-relay, never caches streams) - CSP gains worker-src + manifest-src; relay sends no-store on /relay + /probe so the SW is the sole metadata freshness authority; webmanifest MIME fix Docs: README de-emojified (title only) and reframed as a peer parallel build. Verified: SSRF probes blocked, legit probe + cache, new headers present, PWA assets served, parser tests 8/8, JS syntax clean, zero Rust warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
159 lines
10 KiB
Markdown
159 lines
10 KiB
Markdown
<div align="center">
|
||
|
||
# 🔮 The Pondering Orb
|
||
|
||
**A retro CRT-themed web TV. Channel-surf live streams through a security-hardened, same-origin relay.**
|
||
|
||
*A reformed mad-wizard gazes into a scrying orb and flips channels like it's 1994 — but every upstream is treated as hostile.*
|
||
|
||
[](#tech-stack)
|
||
[-e10000?style=flat-square)](#the-relay)
|
||
[](#security-posture)
|
||
[](#license)
|
||
|
||
</div>
|
||
|
||
---
|
||
|
||
## What it is
|
||
|
||
The Pondering Orb turns any HLS source into a 1994 cable-TV experience in the browser: a red/black phosphor CRT frame, a scrolling TV guide, channel zapping with a 7-segment OSD, and YouTube-style theater / dim / ambient-glow modes.
|
||
|
||
It is built around one idea: **a channel is "a named thing on a guide that resolves to an HLS stream at a point in time."** Public live streams (via [iptv-org](https://github.com/iptv-org/iptv)) and — later — your own self-hosted media library are just different *providers* feeding the same UI. No rewrite required to add the second.
|
||
|
||
> **"Purple team"** here is a security *identity* (red offense + blue defense), not a color. The palette stays red/black; the wizard theme is naming and lore on top of clear, accessible function.
|
||
|
||
It is developed as a parallel build to a peer project that takes the same premise — *your media as 24/7 TV channels* — through a different architecture (a NAS + ErsatzTV + Raspberry Pi feeding a physical CRT). This implementation is portable, browser-based, and security-first.
|
||
|
||
## Features
|
||
|
||
- **Authentic CRT presentation** — phosphor glow, scanlines, screen curvature, vignette breathing (all CSS, no WebGL).
|
||
- **Cable-box channel zapping** — number entry with debounce + 7-segment OSD, channel up/down, last-channel toggle, auto-fading now-playing banner.
|
||
- **"Scrying Static" transitions** — procedural channel-change noise that clears the moment the new stream buffers (reduced-motion aware).
|
||
- **The Almanac** — a virtualized, 1994-style scrolling TV guide with now/next, a live current-time playhead, and keyboard navigation. Scales to tens of thousands of channels.
|
||
- **Immersion stack** — Theater, Dim, and Ambient-backlight modes (à la YouTube), composable and keyboard-driven.
|
||
- **Graceful dead-stream handling** — "the orb clouds over" no-signal card, automatic fallback URLs, and auto-skip.
|
||
- **The Lexicon** — optional in-world vocabulary (guide → *The Almanac*, search → *Scry*) with a one-click "mundane mode"; screen-reader labels always stay plain-English.
|
||
- **Accessible by default** — full keyboard map + help overlay, ARIA roles/live regions, visible focus rings, and a complete `prefers-reduced-motion` kill-switch.
|
||
|
||
## Security posture
|
||
|
||
Every stream URL comes from a public, third-party, mutable playlist — so the Orb treats all of it as hostile input. The browser only ever talks to its own origin; the Rust relay is the single egress point, and all SSRF risk is concentrated in one auditable module.
|
||
|
||
| Control | Implementation |
|
||
| --- | --- |
|
||
| **SSRF defense** | Resolves DNS itself, rejects the *whole host* if **any** resolved IP is private / loopback / link-local / CGNAT / cloud-metadata (`169.254.169.254`) / IPv6 ULA+link-local, then connects to that **exact validated IP** — no re-resolve, defeating DNS-rebinding. Fail-closed. |
|
||
| **HTTPS-only egress** | Non-`https` upstreams are refused outright. |
|
||
| **Bounded redirects** | Each redirect hop is re-validated through the same SSRF gate. |
|
||
| **Strict CSP** | `default-src 'none'`; no `unsafe-inline` / `unsafe-eval`; scripting, styling, and XHR all locked to `'self'`. |
|
||
| **Untrusted-input rendering** | Playlist/EPG fields are inserted as text only (never `innerHTML`); URL schemes allow-listed; field/length caps guard against client-side DoS. |
|
||
| **Hardened serving** | Path-traversal-safe static handler, `nosniff`, `no-referrer`, `X-Frame-Options: DENY`, locked `Permissions-Policy`. |
|
||
| **Supply chain** | HLS.js pinned to an exact version and Subresource-Integrity verified. |
|
||
| **Observability** | `/metrics` (Prometheus) counts relay outcomes including SSRF blocks; structured JSON logs record host + outcome, **never** the raw URL (query strings can carry stream tokens). |
|
||
|
||
### Verified
|
||
|
||
The current build passes its full probe battery: all SSRF vectors rejected (`403`/`400`), CSP headers present with no `unsafe-*`, path traversal blocked, a real Apple HLS manifest fetched and its nested URLs rewritten through the relay, and the live iptv-org catalog (~39k channels) retrieved end-to-end through the hardened path. Rust unit tests and JS syntax checks are green with zero build warnings.
|
||
|
||
## Quick start
|
||
|
||
**Full experience** (serves the SPA *and* the relay from one origin):
|
||
|
||
```sh
|
||
cd server
|
||
cargo run --release
|
||
# → http://127.0.0.1:8088
|
||
```
|
||
|
||
**Front-end only** (no relay; uses the built-in mock realm and CORS-clean test streams):
|
||
|
||
```sh
|
||
cd web
|
||
python3 -m http.server 8099
|
||
# → http://127.0.0.1:8099
|
||
```
|
||
|
||
### Keyboard
|
||
|
||
| Key | Action | | Key | Action |
|
||
| --- | --- | --- | --- | --- |
|
||
| `0–9` | Tune channel | | `T` | Theater mode |
|
||
| `↑ / ↓` | Volume · guide nav | | `D` | Dim mode |
|
||
| `PgUp/PgDn` | Channel up / down | | `A` | Ambient glow |
|
||
| `Backspace` | Last channel | | `F` | Fullscreen |
|
||
| `G` | Toggle the Almanac | | `?` | Help |
|
||
|
||
Append `?provider=mock` or `?provider=iptv` to force a data source.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Browser (same origin — no CORS, untainted ambient canvas) │
|
||
│ │
|
||
│ index.html ── app.js ──┬── player.js (HLS.js, controls) │
|
||
│ ├── zapper.js (tuning, OSD) │
|
||
│ ├── guide.js (virtualized EPG) │
|
||
│ ├── ambient.js (backlight) │
|
||
│ └── providers/ │
|
||
│ ├── source-provider.js ◄── the │
|
||
│ ├── mock.js interface
|
||
│ └── iptv-org.js │
|
||
└───────────────────────────────┬─────────────────────────────┘
|
||
│ / · /relay · /metrics
|
||
┌───────────────────────────────▼─────────────────────────────┐
|
||
│ Rust server (binds 127.0.0.1) │
|
||
│ main.rs static serving + security headers + observability│
|
||
│ relay.rs HLS proxy + manifest URL rewrite + redirects │
|
||
│ ssrf.rs resolve → validate IP → connect (sole egress) │
|
||
└───────────────────────────────┬─────────────────────────────┘
|
||
▼
|
||
Public HLS origins · iptv-org JSON API
|
||
```
|
||
|
||
Everything in the UI talks only to the `SourceProvider` contract — never directly to iptv-org or any one source. `MockProvider` was built first to keep the UI provably source-agnostic; a future `LocalMediaProvider` (your own library, packaged to HLS) drops in alongside it.
|
||
|
||
### Tech stack
|
||
|
||
- **Front end:** vanilla JavaScript (ES modules) + [HLS.js](https://github.com/video-dev/hls.js) — no framework, no build step.
|
||
- **Relay:** Rust — [hyper](https://hyper.rs) 1.x, [tokio](https://tokio.rs), [rustls](https://github.com/rustls/rustls) (ring), [hickory-resolver](https://github.com/hickory-dns/hickory-dns).
|
||
|
||
## Roadmap
|
||
|
||
**Now (v1):** hardened same-origin relay · iptv-org provider · zapping · virtualized guide · theater/dim/ambient · dead-stream handling · observability.
|
||
|
||
**Next:** stream health prober + scoring · session resurrection · off-thread XMLTV parsing · service-worker PWA.
|
||
|
||
**Later:** *The Scryer's Lens* (live stream trust/provenance overlay) · *The Chronicle* (channel-lineup-as-code with diff/audit) · *Cursed Channels* (adversarial test harness that proves the defenses) · *The Wizard's Workbench* (compose your own channels; export as a signed "Spellbook") · `LocalMediaProvider` for self-hosted media.
|
||
|
||
## Project structure
|
||
|
||
```
|
||
web/ Static SPA
|
||
index.html CRT shell + markup
|
||
styles.css Red/black CRT theme (all effects, responsive, a11y)
|
||
app.js Bootstrap, global state, keyboard map, modes
|
||
player.js HLS.js wiring, ghost controls, dead-stream handling
|
||
zapper.js Channel tuning, OSD, scrying-static transition
|
||
guide.js Virtualized EPG overlay
|
||
ambient.js Ambient backlight canvas
|
||
lexicon.js In-world vocabulary + mundane-mode
|
||
providers/ SourceProvider contract + Mock + iptv-org
|
||
lib/ Pinned, SRI-verified HLS.js
|
||
server/ Rust same-origin server + hardened relay
|
||
src/{main,relay,ssrf}.rs
|
||
```
|
||
|
||
## Disclaimer
|
||
|
||
This project does **not** host, store, or transmit any media. It indexes **publicly available** stream URLs aggregated by the [iptv-org](https://github.com/iptv-org/iptv) project. You are solely responsible for ensuring any stream you access is licensed and lawful in your jurisdiction.
|
||
|
||
## Acknowledgments
|
||
|
||
- Developed as a parallel exploration alongside a peer's NAS-to-CRT "media as 24/7 channels" project.
|
||
- Channel data and the public stream index come from the [iptv-org](https://github.com/iptv-org/iptv) community project.
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE) — code only. Channel data and streams are the property of their respective owners.
|