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);
+ });
+});