DR-235 phase 3. Every video renderer is native now: mpv on Linux and Windows, ExoPlayer on Android, all drawing behind the transparent webview. The HTML5 <video> path is gone, not bypassed: - Frontend: hls.js, Html5PlayerAdapter and its compatibility shim, the createAdapter factory, streamTransport, hlsRecovery, timeTracking, videoFit, the <video>/<track> markup and every element handler in VideoPlayer (3277 -> 2144 lines), the experimentalNativeVideo store and its Settings toggle, webviewVideoFallback/supportsNativeVideo, and the setHtml5VideoState PiP bridge call. NativePlayerAdapter is the one video adapter; webview audio gets its own adapter kind. - Rust: use_html5 dropped from player_seek_video, player_switch_audio_track and player_set_stream_quality with the Html5* strategies and ReloadStream responses; use_html5_element and VideoBackend dropped from PlayerStatus; player_play_item always loads the backend (set_current_item removed); Capabilities::webview removed; the WebKitGTK GStreamer/VAAPI setup (and its gst-inspect spawn) removed. - Android: the HTML5 video state in PictureInPictureManager and ScreenWakeManager, and the bridge method feeding it. - CSP: connect-src loses http:/https: and worker-src loses blob: - both existed for hls.js; with it gone they were only an exfiltration channel and a blob worker for injected script. A test now keeps them out. mpv takes over what the <video> element did (mpv_tracks, UT-275): subtitles are the WebVTT list the play request carries, queued on sub-files and selected by position in that list, starting off; audio tracks are selected by position in the file; sid/aid are reset before each load. Without this, Linux video had no subtitle selection and a direct-play audio switch failed since mpv became its renderer. Verified: Rust 948 passing, and the same 948 cross-compiled for Windows under wine against the shipped DLL (track tests included); frontend 1111 passing; aarch64 debug APK builds. Lint warnings 158 -> 146, CI ratchet tightened to match. Not yet seen on Windows hardware.
101 lines
4.0 KiB
TypeScript
101 lines
4.0 KiB
TypeScript
/**
|
|
* Guards the shipped webview security configuration.
|
|
*
|
|
* `csp` was `null` and the asset protocol was scoped to the whole storage root,
|
|
* which is the directory holding the SQLite database and the encrypted-token
|
|
* fallback file. Both are one-character regressions away and neither is visible
|
|
* in any behavioural test, so they are asserted here instead: the restrictive
|
|
* half of the policy must stay restrictive, and the permissive half must keep
|
|
* the schemes playback actually needs.
|
|
*
|
|
* TRACES: UR-012, UR-071 | DR-198 | UT-193
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { readFileSync } from "fs";
|
|
import { resolve } from "path";
|
|
|
|
const config = JSON.parse(
|
|
readFileSync(resolve(__dirname, "../src-tauri/tauri.conf.json"), "utf-8"),
|
|
);
|
|
|
|
const security = config.app.security;
|
|
|
|
/** Split a CSP string into `directive -> sources`. */
|
|
function directives(csp: string): Record<string, string[]> {
|
|
const map: Record<string, string[]> = {};
|
|
for (const part of csp.split(";")) {
|
|
const [name, ...sources] = part.trim().split(/\s+/);
|
|
if (name) map[name] = sources;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
describe("tauri.conf.json CSP", () => {
|
|
it("is set at all — a null CSP hands any injected script the full IPC surface", () => {
|
|
expect(typeof security.csp).toBe("string");
|
|
expect(security.csp.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
const csp = directives(security.csp as string);
|
|
|
|
it("locks down script execution", () => {
|
|
// Tauri injects a nonce for SvelteKit's inline bootstrap script at build
|
|
// time, so 'self' alone is enough and inline/eval must never be re-added.
|
|
expect(csp["script-src"]).toEqual(["'self'"]);
|
|
expect(csp["object-src"]).toEqual(["'none'"]);
|
|
expect(csp["frame-src"]).toEqual(["'none'"]);
|
|
expect(csp["base-uri"]).toEqual(["'self'"]);
|
|
expect(csp["default-src"]).toEqual(["'self'"]);
|
|
});
|
|
|
|
it("keeps the schemes playback and thumbnails depend on", () => {
|
|
// The asset protocol under both names convertFileSrc emits.
|
|
expect(csp["img-src"]).toContain("asset:");
|
|
expect(csp["img-src"]).toContain("http://asset.localhost");
|
|
expect(csp["media-src"]).toContain("asset:");
|
|
// The token-guarded loopback media server (DR-137).
|
|
expect(csp["media-src"]).toContain("http://127.0.0.1:*");
|
|
// Tauri's invoke transport.
|
|
expect(csp["connect-src"]).toContain("ipc:");
|
|
expect(csp["connect-src"]).toContain("http://ipc.localhost");
|
|
// The user's Jellyfin server: an arbitrary run-time origin, http on a LAN.
|
|
for (const directive of ["img-src", "media-src"]) {
|
|
expect(csp[directive]).toContain("http:");
|
|
expect(csp[directive]).toContain("https:");
|
|
}
|
|
});
|
|
|
|
// The page makes no network requests of its own — all traffic goes through
|
|
// Rust — so `connect-src` is IPC only. It allowed any http(s) host while
|
|
// hls.js fetched segments in the page; with hls.js deleted (DR-235) that
|
|
// grant was only an exfiltration channel for injected script. Likewise the
|
|
// blob worker was hls.js' demuxer.
|
|
it("gives injected script no network egress and no blob workers", () => {
|
|
expect(csp["connect-src"]).not.toContain("http:");
|
|
expect(csp["connect-src"]).not.toContain("https:");
|
|
expect(csp["worker-src"]).not.toContain("blob:");
|
|
});
|
|
|
|
it("never widens a data directive into script execution", () => {
|
|
for (const [name, sources] of Object.entries(csp)) {
|
|
if (name === "script-src" || name === "worker-src") {
|
|
expect(sources).not.toContain("'unsafe-eval'");
|
|
expect(sources).not.toContain("'unsafe-inline'");
|
|
}
|
|
// A bare `*` would re-admit every scheme, including file:.
|
|
expect(sources).not.toContain("*");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("tauri.conf.json asset protocol scope", () => {
|
|
const scope: string[] = security.assetProtocol.scope;
|
|
|
|
it("covers only the thumbnail cache, not the storage root", () => {
|
|
expect(scope).toEqual(["$APPDATA/thumbnails/**"]);
|
|
// The database and the encrypted-token fallback live directly in $APPDATA.
|
|
expect(scope).not.toContain("$APPDATA/**");
|
|
});
|
|
});
|