#!/usr/bin/env node /* The direct-playability probe, tested without a browser. * * node tests/unit_direct_probe.js * * The probe decides whether the player may skip the proxy and let the browser * fetch a CDN URL itself. Getting that wrong is not a slow video but a wrong * one: eporner's CDN answers a browser-origin Referer with a 302 to a * one-minute "watch this at our site instead" clip, served 206 with * `Access-Control-Allow-Origin: *`, so "bytes arrived" was satisfied by a * stand-in and the player switched to it mid-video. What settles it is whether * the direct route hands back the same media the proxied one does, which is * two stubbed fetches to check -- no server, no Chromium, no network. */ const fs = require('fs'); const path = require('path'); const vm = require('vm'); const noop = () => {}; const element = () => ({ style: { setProperty: noop, removeProperty: noop }, classList: { add: noop, remove: noop, toggle: noop, contains: () => false }, dataset: {}, querySelector: () => null, querySelectorAll: () => [], addEventListener: noop, removeEventListener: noop, appendChild: noop, removeChild: noop, remove: noop, getBoundingClientRect: () => ({ width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0 }), setAttribute: noop, removeAttribute: noop, getAttribute: () => null, cloneNode: element, content: { firstElementChild: { cloneNode: element } }, children: [], childElementCount: 0, }); const sandbox = { console, setTimeout, clearTimeout, URL, Response, AbortController, Image: function () { return element(); }, IntersectionObserver: function () { return { observe: noop, unobserve: noop, disconnect: noop }; }, requestAnimationFrame: noop, requestIdleCallback: noop, performance: { now: () => 0 }, localStorage: { getItem: () => null, setItem: noop, removeItem: noop }, }; sandbox.addEventListener = noop; sandbox.removeEventListener = noop; sandbox.window = sandbox; sandbox.self = sandbox; sandbox.globalThis = sandbox; sandbox.document = { getElementById: () => null, createElement: element, querySelectorAll: () => [], addEventListener: noop, documentElement: element(), body: element(), head: element(), }; sandbox.window.matchMedia = () => ({ matches: false, addEventListener: noop }); sandbox.window.location = { href: 'http://localhost/' }; const context = vm.createContext(sandbox); sandbox.App = { state: {}, constants: {}, favorites: { getKey: () => null, has: () => false, setButtonState: noop }, storage: { getPreferredQuality: () => 'auto' } }; vm.runInContext( fs.readFileSync(path.join(__dirname, '..', 'frontend', 'js', 'videos.js'), 'utf8'), context, 'videos.js'); const { probeDirect } = sandbox.App.videos; let failed = 0; const ok = (label, cond, detail) => { if (!cond) failed++; console.log(` [${cond ? 'PASS' : 'FAIL'}] ${label}` + (!cond && detail ? ` -- ${detail}` : '')); }; // Stands in for both routes. `direct` is what the CDN hands the browser, // `proxied` what /api/stream reports -- either given as a byte count, or null // for a response that states no length at all. Every call is recorded so the // test can check what was asked of whom. let calls = []; const stubFetch = function(direct, proxied) { calls = []; sandbox.fetch = async (url, opts) => { calls.push({ url: String(url), method: (opts && opts.method) || 'GET' }); if (String(url).indexOf('/api/stream') === 0) { const headers = proxied === null ? {} : { 'Content-Length': String(proxied) }; return new Response('', { status: 200, headers: headers }); } const headers = direct === null ? {} : { 'Content-Length': String(direct) }; return new Response(new Uint8Array(16), { status: 206, headers: headers }); }; }; // A fresh host each time: the probe caches its answer per host for the session, // which is the point of it, so reusing one would answer from the cache. let hostSeq = 0; const sourceOn = (host) => ({ url: `https://${host}/v4/token/10299874-1080p.mp4`, referer: 'https://www.eporner.com/', headers: { Referer: 'https://www.eporner.com/' }, }); const freshSource = () => sourceOn(`cdn${++hostSeq}.example.com`); const run = async () => { console.log('\nwhat the two routes say about the same URL'); stubFetch(486933813, 486933813); ok('the same length both ways is a shortcut', await probeDirect(freshSource()) === true); // The bug: 642497 is static.eporner.com/na.mp4, the stand-in clip. stubFetch(642497, 486933813); ok('a shorter file than the proxy reports is a substitution', await probeDirect(freshSource()) === false); // Not every mismatch is a decoy -- a longer one is just as wrong. stubFetch(486933813, 642497); ok('any disagreement about length is refused', await probeDirect(freshSource()) === false); console.log('\nwhen a length cannot be had'); stubFetch(642497, null); ok('a proxy that states no length leaves the bytes test standing', await probeDirect(freshSource()) === true); stubFetch(null, 486933813); ok('a direct route that states no length is not accused', await probeDirect(freshSource()) === true); stubFetch(null, null); ok('neither stating one still plays direct', await probeDirect(freshSource()) === true); console.log('\nhow the cross-check is addressed'); stubFetch(486933813, 486933813); const src = freshSource(); await probeDirect(src); const head = calls.find((c) => c.url.indexOf('/api/stream') === 0); ok('the proxy is asked, by HEAD', !!head && head.method === 'HEAD', head ? head.method : 'never asked'); ok('with the Referer the origin wants, not the page\'s', !!head && head.url.indexOf(`referer=${encodeURIComponent('https://www.eporner.com/')}`) !== -1, head ? head.url : ''); ok('about the very URL being probed', !!head && head.url.indexOf(encodeURIComponent(src.url)) !== -1); console.log('\nthe answer is kept per host'); stubFetch(486933813, 486933813); const repeat = sourceOn('cached.example.com'); await probeDirect(repeat); const afterFirst = calls.length; await probeDirect(repeat); ok('a second probe of a known host asks nobody', calls.length === afterFirst, `${calls.length - afterFirst} extra call(s)`); console.log('\na bare URL still works'); stubFetch(642497, 486933813); ok('a string is probed, and still cross-checked', await probeDirect('https://bare.example.com/v.mp4') === false); stubFetch(486933813, 486933813); ok('nothing to probe is not playable', await probeDirect('') === false); ok('neither is a source without a URL', await probeDirect({ url: '' }) === false); }; run().then(() => { console.log(`\n${failed ? 'FAILED' : 'OK'}: ${failed} check(s) failed`); process.exit(failed ? 1 : 0); }).catch((err) => { console.error(err); process.exit(1); });