openvibers/openvibe.live Commits
Links to Live's own pages given as full addresses work (the navbar's My channel opened /https://openvibe.live/@name): navigate() takes their path, and a glued own address 301s to it (#422)
Changes
@@@ -945,8 +945,15 @@ function _ovHoldsMedia() {/** A URL that leaves Live (an OpenVibe.Video watch page, another OpenVibe site): the browser goes there, not the SPA. */const isOffSite = (url) => /^https?:\/\//i.test(String(url || '')) && !String(url).startsWith(`${location.origin}/`);++/** A full address on Live itself (the shared navbar's "My channel" is https://openvibe.live/@name) as its path. */++function sameSitePath(url) {++ if (!/^https?:\/\//i.test(String(url || ''))) return url;++ try { const u = new URL(url); return u.origin === location.origin ? `${u.pathname}${u.search}${u.hash}` : url; } catch { return url; }++}++function navigate(urlPath, replace = false) {if (isOffSite(urlPath)) { if (replace) location.replace(urlPath); else location.href = urlPath; return; }++ urlPath = sameSitePath(urlPath);if (_ovRelease.stale && !_ovHoldsMedia()) {const to = urlPath.startsWith('/') ? urlPath : `/${urlPath}`;try { sessionStorage.setItem('ov-release-load', _ovRelease.stale); } catch { /* */ }@@@ -28,11 +28,14 @@ function secondsOf(t) {// which put any link it was handed under '/', and the API now hands it these absolute links.const GLUED_RE = /^\/https?:\/\/?([^/?#]+)(\/[^?#]*)?$/i;--/** Where a glued path really points, on one of the two sites Live hands pages to; null for anything else. */--function unglue(pathname, search = '') {++/** Where a glued path really points, on one of the two sites Live hands pages to, or Live's own page (its path, when++ * ownHost names the host it was glued onto: a router that put a full openvibe.live link under '/'); null otherwise. */++function unglue(pathname, search = '', ownHost = null) {const m = GLUED_RE.exec(String(pathname || ''));if (!m) return null;const host = m[1].toLowerCase();++ // One leading slash: //evil.example/x would leave the site.++ if (ownHost && host === String(ownHost).toLowerCase()) return `/${(m[2] || '/').replace(/^\/+/, '')}${search}`;const origin = [VIDEO_BASE, communityUrl()].find((o) => { try { return new URL(o).host === host; } catch { return false; } });return origin ? `${origin}${m[2] || '/'}${search}` : null;}@@@ -42,7 +45,7 @@ function unglue(pathname, search = '') {function register(app) {app.get(/^\/https?:\//i, (req, res, next) => {const q = req.originalUrl.indexOf('?');-- const to = unglue(req.path, q >= 0 ? req.originalUrl.slice(q) : '');++ const to = unglue(req.path, q >= 0 ? req.originalUrl.slice(q) : '', req.get('host'));return to ? res.redirect(301, to) : next();});for (const [prefix, kind] of [['vod', 'vod'], ['clip', 'clip']]) {@@@ -47,6 +47,30 @@ process.env.NODE_ENV = 'test';assert.strictEqual((await get('/https:openvibe.video/v/1')).status, 404);});++ await check('Live\'s own full address glued onto Live\'s (the navbar\'s "My channel" through the old router) goes to its path, never off the site', async () => {++ const { unglue } = require('../server/web/video-handover');++ assert.strictEqual(unglue('/https://openvibe.live/@goosely', '', 'openvibe.live'), '/@goosely');++ assert.strictEqual(unglue('/https:/openvibe.live/@goosely', '?tab=videos', 'openvibe.live'), '/@goosely?tab=videos');++ assert.strictEqual(unglue('/https://openvibe.live', '', 'openvibe.live'), '/');++ assert.strictEqual(unglue('/https://openvibe.live//evil.example/x', '', 'openvibe.live'), '/evil.example/x', 'one leading slash: it stays on Live');++ assert.strictEqual(unglue('/https://openvibe.live/@goosely', '', null), null, 'no own host: not a redirect');++ assert.strictEqual(unglue('/https://evil.example/@x', '', 'openvibe.live'), null);++ const host = `127.0.0.1:${server.address().port}`;++ assert.deepStrictEqual(await get(`/https://${host}/@goosely`), { status: 301, location: '/@goosely' });++ });++++ await check('the SPA router takes a full address on Live as its path (public/js/app.js sameSitePath)', async () => {++ const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');++ const fn = /function sameSitePath\(url\) \{[\s\S]*?\n\}/.exec(src);++ assert.ok(fn, 'sameSitePath is in app.js');++ assert.match(src, /urlPath = sameSitePath\(urlPath\);/, 'navigate() uses it');++ const sameSitePath = new Function('location', `${fn[0]}; return sameSitePath;`)({ origin: 'https://openvibe.live' });++ assert.strictEqual(sameSitePath('https://openvibe.live/@goosely'), '/@goosely');++ assert.strictEqual(sameSitePath('https://openvibe.live/@goosely?tab=videos#top'), '/@goosely?tab=videos#top');++ assert.strictEqual(sameSitePath('/content'), '/content');++ assert.strictEqual(sameSitePath('https://openvibe.video/v/1'), 'https://openvibe.video/v/1', 'another site is left alone');++ });++await check('what is not an id stays Live\'s 404', async () => {assert.strictEqual((await get('/vod/abc')).status, 404);assert.strictEqual((await get('/vod/0')).status, 404);
@@ -945,8 +945,15 @@ function _ovHoldsMedia() {
@@ -945,8 +945,15 @@ function _ovHoldsMedia() {
/** A URL that leaves Live (an OpenVibe.Video watch page, another OpenVibe site): the browser goes there, not the SPA. */
/** A URL that leaves Live (an OpenVibe.Video watch page, another OpenVibe site): the browser goes there, not the SPA. */
const isOffSite = (url) => /^https?:\/\//i.test(String(url || '')) && !String(url).startsWith(`${location.origin}/`);
const isOffSite = (url) => /^https?:\/\//i.test(String(url || '')) && !String(url).startsWith(`${location.origin}/`);
+/** A full address on Live itself (the shared navbar's "My channel" is https://openvibe.live/@name) as its path. */
+function sameSitePath(url) {
+ if (!/^https?:\/\//i.test(String(url || ''))) return url;
+ try { const u = new URL(url); return u.origin === location.origin ? `${u.pathname}${u.search}${u.hash}` : url; } catch { return url; }
+}
+
function navigate(urlPath, replace = false) {
function navigate(urlPath, replace = false) {
if (isOffSite(urlPath)) { if (replace) location.replace(urlPath); else location.href = urlPath; return; }
if (isOffSite(urlPath)) { if (replace) location.replace(urlPath); else location.href = urlPath; return; }
+ urlPath = sameSitePath(urlPath);
if (_ovRelease.stale && !_ovHoldsMedia()) {
if (_ovRelease.stale && !_ovHoldsMedia()) {
const to = urlPath.startsWith('/') ? urlPath : `/${urlPath}`;
const to = urlPath.startsWith('/') ? urlPath : `/${urlPath}`;
try { sessionStorage.setItem('ov-release-load', _ovRelease.stale); } catch { /* */ }
try { sessionStorage.setItem('ov-release-load', _ovRelease.stale); } catch { /* */ }
@@ -28,11 +28,14 @@ function secondsOf(t) {
@@ -28,11 +28,14 @@ function secondsOf(t) {
// which put any link it was handed under '/', and the API now hands it these absolute links.
// which put any link it was handed under '/', and the API now hands it these absolute links.
const GLUED_RE = /^\/https?:\/\/?([^/?#]+)(\/[^?#]*)?$/i;
const GLUED_RE = /^\/https?:\/\/?([^/?#]+)(\/[^?#]*)?$/i;
-/** Where a glued path really points, on one of the two sites Live hands pages to; null for anything else. */
-function unglue(pathname, search = '') {
+/** Where a glued path really points, on one of the two sites Live hands pages to, or Live's own page (its path, when
+ * ownHost names the host it was glued onto: a router that put a full openvibe.live link under '/'); null otherwise. */
+function unglue(pathname, search = '', ownHost = null) {
const m = GLUED_RE.exec(String(pathname || ''));
const m = GLUED_RE.exec(String(pathname || ''));
if (!m) return null;
if (!m) return null;
const host = m[1].toLowerCase();
const host = m[1].toLowerCase();
+ // One leading slash: //evil.example/x would leave the site.
+ if (ownHost && host === String(ownHost).toLowerCase()) return `/${(m[2] || '/').replace(/^\/+/, '')}${search}`;
const origin = [VIDEO_BASE, communityUrl()].find((o) => { try { return new URL(o).host === host; } catch { return false; } });
const origin = [VIDEO_BASE, communityUrl()].find((o) => { try { return new URL(o).host === host; } catch { return false; } });
return origin ? `${origin}${m[2] || '/'}${search}` : null;
return origin ? `${origin}${m[2] || '/'}${search}` : null;
}
}
@@ -42,7 +45,7 @@ function unglue(pathname, search = '') {
@@ -42,7 +45,7 @@ function unglue(pathname, search = '') {
function register(app) {
function register(app) {
app.get(/^\/https?:\//i, (req, res, next) => {
app.get(/^\/https?:\//i, (req, res, next) => {
const q = req.originalUrl.indexOf('?');
const q = req.originalUrl.indexOf('?');
- const to = unglue(req.path, q >= 0 ? req.originalUrl.slice(q) : '');
+ const to = unglue(req.path, q >= 0 ? req.originalUrl.slice(q) : '', req.get('host'));
return to ? res.redirect(301, to) : next();
return to ? res.redirect(301, to) : next();
});
});
for (const [prefix, kind] of [['vod', 'vod'], ['clip', 'clip']]) {
for (const [prefix, kind] of [['vod', 'vod'], ['clip', 'clip']]) {
@@ -47,6 +47,30 @@ process.env.NODE_ENV = 'test';
@@ -47,6 +47,30 @@ process.env.NODE_ENV = 'test';
assert.strictEqual((await get('/https:openvibe.video/v/1')).status, 404);
assert.strictEqual((await get('/https:openvibe.video/v/1')).status, 404);
});
});
+ await check('Live\'s own full address glued onto Live\'s (the navbar\'s "My channel" through the old router) goes to its path, never off the site', async () => {
+ const { unglue } = require('../server/web/video-handover');
+ assert.strictEqual(unglue('/https://openvibe.live/@goosely', '', 'openvibe.live'), '/@goosely');
+ assert.strictEqual(unglue('/https:/openvibe.live/@goosely', '?tab=videos', 'openvibe.live'), '/@goosely?tab=videos');
+ assert.strictEqual(unglue('/https://openvibe.live', '', 'openvibe.live'), '/');
+ assert.strictEqual(unglue('/https://openvibe.live//evil.example/x', '', 'openvibe.live'), '/evil.example/x', 'one leading slash: it stays on Live');
+ assert.strictEqual(unglue('/https://openvibe.live/@goosely', '', null), null, 'no own host: not a redirect');
+ assert.strictEqual(unglue('/https://evil.example/@x', '', 'openvibe.live'), null);
+ const host = `127.0.0.1:${server.address().port}`;
+ assert.deepStrictEqual(await get(`/https://${host}/@goosely`), { status: 301, location: '/@goosely' });
+ });
+
+ await check('the SPA router takes a full address on Live as its path (public/js/app.js sameSitePath)', async () => {
+ const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
+ const fn = /function sameSitePath\(url\) \{[\s\S]*?\n\}/.exec(src);
+ assert.ok(fn, 'sameSitePath is in app.js');
+ assert.match(src, /urlPath = sameSitePath\(urlPath\);/, 'navigate() uses it');
+ const sameSitePath = new Function('location', `${fn[0]}; return sameSitePath;`)({ origin: 'https://openvibe.live' });
+ assert.strictEqual(sameSitePath('https://openvibe.live/@goosely'), '/@goosely');
+ assert.strictEqual(sameSitePath('https://openvibe.live/@goosely?tab=videos#top'), '/@goosely?tab=videos#top');
+ assert.strictEqual(sameSitePath('/content'), '/content');
+ assert.strictEqual(sameSitePath('https://openvibe.video/v/1'), 'https://openvibe.video/v/1', 'another site is left alone');
+ });
+
await check('what is not an id stays Live\'s 404', async () => {
await check('what is not an id stays Live\'s 404', async () => {
assert.strictEqual((await get('/vod/abc')).status, 404);
assert.strictEqual((await get('/vod/abc')).status, 404);
assert.strictEqual((await get('/vod/0')).status, 404);
assert.strictEqual((await get('/vod/0')).status, 404);