From aaebd09e6791449e68db871a19347d088bacebd8 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Sun, 25 Oct 2026 19:52:04 +0100 Subject: [PATCH] feat: youtube embed wrapper for apps without an https origin --- src/index.ts | 3 +++ src/youtube.ts | 20 +++++++++++++++++++ test/youtube.spec.ts | 47 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/youtube.ts create mode 100644 test/youtube.spec.ts diff --git a/src/index.ts b/src/index.ts index ae78f09..cabc963 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import { toMatrixID } from './mxcId'; import { previewUrl } from './preview'; import { proxyMediaCall } from './proxy'; import { returnMatrixServerVers } from './serverversion'; +import { youtubeEmbed } from './youtube'; import { MatrixWellKnownServer, SoliditasAddressConvertResponse } from './types'; export default { @@ -40,6 +41,8 @@ export default { return proxyMediaCall(mediaId); } else if (url.pathname === '/_soliditas/preview_url') { return previewUrl(url.searchParams.get('url'), serverName); + } else if (url.pathname === '/_soliditas/embed/youtube') { + return youtubeEmbed(url.searchParams); } else if (url.pathname === '/_soliditas/adressconvert'){ // helper function mainly for debug reasons as it should be embeded in the client for production use const remoteType = url.searchParams.get('remoteType'); diff --git a/src/youtube.ts b/src/youtube.ts new file mode 100644 index 0000000..a8b8d63 --- /dev/null +++ b/src/youtube.ts @@ -0,0 +1,20 @@ +export function youtubeEmbed(params: URLSearchParams): Response { + const id = params.get('v'); + const start = params.get('start'); + if (id === null || !/^[A-Za-z0-9_-]{11}$/.test(id) || (start !== null && !/^\d+$/.test(start))) { + return new Response('Invalid video or start time', { status: 400 }); + } + + const src = `https://www.youtube-nocookie.com/embed/${id}?autoplay=1${start === null ? '' : `&start=${start}`}`; + return new Response(` +YouTube + +`, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; frame-src https://www.youtube-nocookie.com; base-uri 'none'; form-action 'none'", + 'Cache-Control': 'public, max-age=86400', + }, + }); +} diff --git a/test/youtube.spec.ts b/test/youtube.spec.ts new file mode 100644 index 0000000..3d2965e --- /dev/null +++ b/test/youtube.spec.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; + +import worker from '../src/index'; + +const env = {} as { SERVERNAME: any; HOSTNAME: any; PORT: any }; + +function embed(query: string) { + return worker.fetch(new Request(`https://gifs.example/_soliditas/embed/youtube?${query}`), env, {}); +} + +describe('youtube embed', () => { + it.each([ + ['', ''], + ['&start=0', '&start=0'], + ['&start=90', '&start=90'], + ])('embeds the video with %s', async (query, start) => { + const response = await embed(`v=MTn_bhTVr2U${query}`); + const html = await response.text(); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('text/html; charset=utf-8'); + expect(html).toContain(`src="https://www.youtube-nocookie.com/embed/MTn_bhTVr2U?autoplay=1${start}"`); + expect(html).toContain('allow="autoplay; encrypted-media; picture-in-picture; fullscreen" allowfullscreen'); + }); + + it('allows custom-scheme parents and sends the service origin to YouTube', async () => { + const response = await embed('v=MTn_bhTVr2U'); + + expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin'); + expect(response.headers.get('Content-Security-Policy')).toBe("default-src 'none'; style-src 'unsafe-inline'; frame-src https://www.youtube-nocookie.com; base-uri 'none'; form-action 'none'"); + expect(response.headers.has('X-Frame-Options')).toBe(false); + expect(response.headers.get('Cache-Control')).toBe('public, max-age=86400'); + }); + + it('accepts the full video id charset', async () => { + expect((await embed('v=aB09_-aB09_')).status).toBe(200); + }); + + it.each([ + '', 'v=', 'v=short', 'v=MTn_bhTVr2Ux', 'v=MTn_bhTVr2%2F', 'v=%22%3E%3Cscript%3E', + 'v=MTn_bhTVr2U%0A', 'v=MTn_bhTVr2U&start=', 'v=MTn_bhTVr2U&start=-1', + 'v=MTn_bhTVr2U&start=1.5', 'v=MTn_bhTVr2U&start=1e2', 'v=MTn_bhTVr2U&start=90s', + 'v=MTn_bhTVr2U&start=%201', 'v=MTn_bhTVr2U&start=1%0A', + ])('rejects %s', async (query) => { + expect((await embed(query)).status).toBe(400); + }); +});