From 8daebe9868b54ecc62f6b2cb7ceb05d50cacacaf Mon Sep 17 00:00:00 2001 From: Dafka <322898297+dafka007@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:04:39 +0300 Subject: [PATCH 1/6] test: define generic public-video URL and unsafe-target contract --- helper/src/universal_site_contract_test.go | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 helper/src/universal_site_contract_test.go diff --git a/helper/src/universal_site_contract_test.go b/helper/src/universal_site_contract_test.go new file mode 100644 index 0000000..83d8ee1 --- /dev/null +++ b/helper/src/universal_site_contract_test.go @@ -0,0 +1,69 @@ +package main + +import "testing" + +// First-slice contract: the user explicitly opens a public video page and +// clicks FramePier. A different supported site must reach the yt-dlp metadata +// probe rather than being refused solely because it is not one of our three +// original integrations. These tests do not perform network requests. +func TestUniversalSiteAcceptsPublicHTTPSPages(t *testing.T) { + urls := []string{ + "https://vimeo.com/123456789", + "https://www.reddit.com/r/videos/comments/abc123/example/", + "https://www.tiktok.com/@creator/video/123456789", + } + for _, raw := range urls { + t.Run(raw, func(t *testing.T) { + got, err := validateMediaURL(raw, "") + if err != nil { + t.Fatalf("public HTTPS page rejected: %v", err) + } + if got.Source != mediaSource("generic") { + t.Fatalf("source = %q; want generic", got.Source) + } + if got.CanonicalURL != raw { + t.Fatalf("canonical URL = %q; want %q", got.CanonicalURL, raw) + } + // Generic sites have no trusted site-specific ID until yt-dlp + // returns validated metadata. + if got.ID != "" { + t.Fatalf("generic ID must not be guessed: %q", got.ID) + } + }) + } +} + +func TestUniversalSiteRejectsUnsafeEntryURLs(t *testing.T) { + cases := []string{ + "http://vimeo.com/123456789", + "https://localhost/watch", + "https://nas.local/watch", + "https://intranet/watch", + "https://metadata.google.internal/latest/meta-data/", + "https://127.0.0.1/watch", + "https://10.0.0.1/watch", + "https://172.16.1.1/watch", + "https://192.168.1.1/watch", + "https://169.254.169.254/latest/meta-data/", + "https://[::1]/watch", + "https://[fc00::1]/watch", + "https://[fe80::1]/watch", + "https://user:password@vimeo.com/123456789", + "https://vimeo.com:8443/123456789", + "file:///C:/Windows/System32/", + "https://vimeo.com/\nInjected: header", + } + for _, raw := range cases { + t.Run(raw, func(t *testing.T) { + if got, err := validateMediaURL(raw, ""); err == nil { + t.Fatalf("unsafe entry URL accepted: %#v", got) + } + }) + } +} + +func TestUniversalSiteDoesNotTrustArbitrarySuppliedVideoID(t *testing.T) { + if got, err := validateMediaURL("https://vimeo.com/123456789", "forged-id"); err == nil { + t.Fatalf("untrusted supplied ID accepted: %#v", got) + } +} From 83db22f1ed0db858731d6ca57b53047a13623da5 Mon Sep 17 00:00:00 2001 From: Dafka <322898297+dafka007@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:16:19 +0300 Subject: [PATCH 2/6] feat: validate bounded generic Vimeo Reddit and TikTok video URLs --- helper/src/url_validation.go | 173 ++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 2 deletions(-) diff --git a/helper/src/url_validation.go b/helper/src/url_validation.go index dea6622..730e5ba 100644 --- a/helper/src/url_validation.go +++ b/helper/src/url_validation.go @@ -14,12 +14,15 @@ const ( sourceTwitchClip mediaSource = "twitch_clip" sourceTwitchVOD mediaSource = "twitch_vod" sourceDailymotion mediaSource = "dailymotion" + sourceGeneric mediaSource = "generic" ) type mediaTarget struct { CanonicalURL string ID string Source mediaSource + // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. + Extractor string } var ( @@ -29,7 +32,146 @@ var ( // Dailymotion video IDs are "x" followed by lowercase base36. The optional // slug that older share links append after "_" is validated and discarded. dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) - dailymotionSlugPattern = regexp.MustCompile(`^[A-Za-z0-9-]{1,200}$`) + dailymotionSlugPattern = regexp.MustCompile(`^[A-Za-z0-9-]{1,200}package main + +import ( + "errors" + "net/url" + "regexp" + "strings" +) + +type mediaSource string + +const ( + sourceYouTube mediaSource = "youtube" + sourceTwitchClip mediaSource = "twitch_clip" + sourceTwitchVOD mediaSource = "twitch_vod" + sourceDailymotion mediaSource = "dailymotion" + sourceGeneric mediaSource = "generic" +) + +type mediaTarget struct { + CanonicalURL string + ID string + Source mediaSource + // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. + Extractor string +} + +var ( + twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) + twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) + twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) + // Dailymotion video IDs are "x" followed by lowercase base36. The optional + // slug that older share links append after "_" is validated and discarded. + dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) +) + vimeoVideoPattern = regexp.MustCompile(`^/[0-9]{1,20}package main + +import ( + "errors" + "net/url" + "regexp" + "strings" +) + +type mediaSource string + +const ( + sourceYouTube mediaSource = "youtube" + sourceTwitchClip mediaSource = "twitch_clip" + sourceTwitchVOD mediaSource = "twitch_vod" + sourceDailymotion mediaSource = "dailymotion" + sourceGeneric mediaSource = "generic" +) + +type mediaTarget struct { + CanonicalURL string + ID string + Source mediaSource + // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. + Extractor string +} + +var ( + twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) + twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) + twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) + // Dailymotion video IDs are "x" followed by lowercase base36. The optional + // slug that older share links append after "_" is validated and discarded. + dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) +) + redditVideoPattern = regexp.MustCompile(`^/r/[A-Za-z0-9_]{1,21}/comments/[A-Za-z0-9]{3,12}/[A-Za-z0-9_-]{1,200}/?package main + +import ( + "errors" + "net/url" + "regexp" + "strings" +) + +type mediaSource string + +const ( + sourceYouTube mediaSource = "youtube" + sourceTwitchClip mediaSource = "twitch_clip" + sourceTwitchVOD mediaSource = "twitch_vod" + sourceDailymotion mediaSource = "dailymotion" + sourceGeneric mediaSource = "generic" +) + +type mediaTarget struct { + CanonicalURL string + ID string + Source mediaSource + // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. + Extractor string +} + +var ( + twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) + twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) + twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) + // Dailymotion video IDs are "x" followed by lowercase base36. The optional + // slug that older share links append after "_" is validated and discarded. + dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) +) + tiktokVideoPattern = regexp.MustCompile(`^/@[A-Za-z0-9_.]{2,32}/video/[0-9]{6,24}package main + +import ( + "errors" + "net/url" + "regexp" + "strings" +) + +type mediaSource string + +const ( + sourceYouTube mediaSource = "youtube" + sourceTwitchClip mediaSource = "twitch_clip" + sourceTwitchVOD mediaSource = "twitch_vod" + sourceDailymotion mediaSource = "dailymotion" + sourceGeneric mediaSource = "generic" +) + +type mediaTarget struct { + CanonicalURL string + ID string + Source mediaSource + // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. + Extractor string +} + +var ( + twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) + twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) + twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) + // Dailymotion video IDs are "x" followed by lowercase base36. The optional + // slug that older share links append after "_" is validated and discarded. + dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) +) ) func validateMediaURL(raw, suppliedID string) (mediaTarget, error) { @@ -49,8 +191,10 @@ func validateMediaURL(raw, suppliedID string) (mediaTarget, error) { target, err = validateParsedTwitchURL(parsed, host) case "dailymotion.com", "www.dailymotion.com": target, err = validateParsedDailymotionURL(parsed) + case "vimeo.com", "www.vimeo.com", "reddit.com", "www.reddit.com", "tiktok.com", "www.tiktok.com": + target, err = validateSupportedGenericURL(parsed, host) default: - err = errors.New("Only official YouTube, Twitch and Dailymotion domains are supported.") + err = errors.New("This site is not yet supported by FramePier.") } if err != nil { return mediaTarget{}, err @@ -127,6 +271,29 @@ func validateParsedDailymotionURL(parsed *url.URL) (mediaTarget, error) { return mediaTarget{}, errors.New("This Dailymotion page is not a supported public video.") } +// This is an explicitly constrained first batch, not an arbitrary-URL pass-through. +// It does not create a network sandbox: yt-dlp's approved extractor may still +// contact media/CDN hosts. Broader site support requires a separate network gate. +func validateSupportedGenericURL(parsed *url.URL, host string) (mediaTarget, error) { + if parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" || parsed.ForceQuery || + parsed.RawPath != "" || parsed.EscapedPath() != parsed.Path { + return mediaTarget{}, errors.New("Use the direct public video URL without extra parameters.") + } + var extractor string + switch host { + case "vimeo.com", "www.vimeo.com": + if vimeoVideoPattern.MatchString(parsed.Path) { extractor = "Vimeo" } + case "reddit.com", "www.reddit.com": + if redditVideoPattern.MatchString(parsed.Path) { extractor = "Reddit" } + case "tiktok.com", "www.tiktok.com": + if tiktokVideoPattern.MatchString(parsed.Path) { extractor = "TikTok" } + } + if extractor == "" { + return mediaTarget{}, errors.New("Open a single public video page on this site, not a feed or profile.") + } + return mediaTarget{CanonicalURL: parsed.String(), Source: sourceGeneric, Extractor: extractor}, nil +} + func pathParts(value string) []string { trimmed := strings.Trim(value, "/") if trimmed == "" { @@ -154,6 +321,8 @@ func sourceLabel(source mediaSource) string { return "Twitch VOD" case sourceDailymotion: return "Dailymotion" + case sourceGeneric: + return "Supported site" default: return "YouTube" } From 490f13f29458b7c267c71eb52d491d67eda634e1 Mon Sep 17 00:00:00 2001 From: Dafka <322898297+dafka007@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:16:40 +0300 Subject: [PATCH 3/6] feat: restrict generic media probes to three named yt-dlp extractors --- helper/src/ytdlp_command.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helper/src/ytdlp_command.go b/helper/src/ytdlp_command.go index e3f91d1..85be9a0 100644 --- a/helper/src/ytdlp_command.go +++ b/helper/src/ytdlp_command.go @@ -43,6 +43,10 @@ func ytdlpExtractorFor(source mediaSource) string { if source == sourceDailymotion { return "dailymotion" } + if source == sourceGeneric { + // Explicit extractors only; never invoke yt-dlp generic/fallback on arbitrary sites. + return "Vimeo,Reddit,TikTok" + } return "" } From 0674fbaeee2979654d800b56c7251919c015c480 Mon Sep 17 00:00:00 2001 From: Dafka <322898297+dafka007@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:43:22 +0300 Subject: [PATCH 4/6] fix: repair malformed Go regex declarations in universal-site validator --- helper/src/url_validation.go | 159 +++-------------------------------- 1 file changed, 11 insertions(+), 148 deletions(-) diff --git a/helper/src/url_validation.go b/helper/src/url_validation.go index 730e5ba..2b13bbb 100644 --- a/helper/src/url_validation.go +++ b/helper/src/url_validation.go @@ -21,8 +21,7 @@ type mediaTarget struct { CanonicalURL string ID string Source mediaSource - // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. - Extractor string + Extractor string // validated extractor for bounded additional sites } var ( @@ -32,146 +31,10 @@ var ( // Dailymotion video IDs are "x" followed by lowercase base36. The optional // slug that older share links append after "_" is validated and discarded. dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) - dailymotionSlugPattern = regexp.MustCompile(`^[A-Za-z0-9-]{1,200}package main - -import ( - "errors" - "net/url" - "regexp" - "strings" -) - -type mediaSource string - -const ( - sourceYouTube mediaSource = "youtube" - sourceTwitchClip mediaSource = "twitch_clip" - sourceTwitchVOD mediaSource = "twitch_vod" - sourceDailymotion mediaSource = "dailymotion" - sourceGeneric mediaSource = "generic" -) - -type mediaTarget struct { - CanonicalURL string - ID string - Source mediaSource - // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. - Extractor string -} - -var ( - twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) - twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) - twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) - // Dailymotion video IDs are "x" followed by lowercase base36. The optional - // slug that older share links append after "_" is validated and discarded. - dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) -) - vimeoVideoPattern = regexp.MustCompile(`^/[0-9]{1,20}package main - -import ( - "errors" - "net/url" - "regexp" - "strings" -) - -type mediaSource string - -const ( - sourceYouTube mediaSource = "youtube" - sourceTwitchClip mediaSource = "twitch_clip" - sourceTwitchVOD mediaSource = "twitch_vod" - sourceDailymotion mediaSource = "dailymotion" - sourceGeneric mediaSource = "generic" -) - -type mediaTarget struct { - CanonicalURL string - ID string - Source mediaSource - // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. - Extractor string -} - -var ( - twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) - twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) - twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) - // Dailymotion video IDs are "x" followed by lowercase base36. The optional - // slug that older share links append after "_" is validated and discarded. - dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) -) - redditVideoPattern = regexp.MustCompile(`^/r/[A-Za-z0-9_]{1,21}/comments/[A-Za-z0-9]{3,12}/[A-Za-z0-9_-]{1,200}/?package main - -import ( - "errors" - "net/url" - "regexp" - "strings" -) - -type mediaSource string - -const ( - sourceYouTube mediaSource = "youtube" - sourceTwitchClip mediaSource = "twitch_clip" - sourceTwitchVOD mediaSource = "twitch_vod" - sourceDailymotion mediaSource = "dailymotion" - sourceGeneric mediaSource = "generic" -) - -type mediaTarget struct { - CanonicalURL string - ID string - Source mediaSource - // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. - Extractor string -} - -var ( - twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) - twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) - twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) - // Dailymotion video IDs are "x" followed by lowercase base36. The optional - // slug that older share links append after "_" is validated and discarded. - dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) -) - tiktokVideoPattern = regexp.MustCompile(`^/@[A-Za-z0-9_.]{2,32}/video/[0-9]{6,24}package main - -import ( - "errors" - "net/url" - "regexp" - "strings" -) - -type mediaSource string - -const ( - sourceYouTube mediaSource = "youtube" - sourceTwitchClip mediaSource = "twitch_clip" - sourceTwitchVOD mediaSource = "twitch_vod" - sourceDailymotion mediaSource = "dailymotion" - sourceGeneric mediaSource = "generic" -) - -type mediaTarget struct { - CanonicalURL string - ID string - Source mediaSource - // Extractor is fixed by a narrowly validated site URL. It is never client-supplied. - Extractor string -} - -var ( - twitchClipIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{6,100}$`) - twitchChannelPattern = regexp.MustCompile(`^[A-Za-z0-9_]{1,25}$`) - twitchVODIDPattern = regexp.MustCompile(`^[0-9]{1,20}$`) - // Dailymotion video IDs are "x" followed by lowercase base36. The optional - // slug that older share links append after "_" is validated and discarded. - dailymotionIDPattern = regexp.MustCompile(`^x[0-9a-z]{4,10}$`) -) + dailymotionSlugPattern = regexp.MustCompile(`^[A-Za-z0-9-]{1,200}$`) + vimeoVideoPattern = regexp.MustCompile(`^/[0-9]{1,20}$`) + redditVideoPattern = regexp.MustCompile(`^/r/[A-Za-z0-9_]{1,21}/comments/[A-Za-z0-9]{3,12}/[A-Za-z0-9_-]{1,200}/?$`) + tiktokVideoPattern = regexp.MustCompile(`^/@[A-Za-z0-9_.]{2,32}/video/[0-9]{6,24}$`) ) func validateMediaURL(raw, suppliedID string) (mediaTarget, error) { @@ -194,7 +57,7 @@ func validateMediaURL(raw, suppliedID string) (mediaTarget, error) { case "vimeo.com", "www.vimeo.com", "reddit.com", "www.reddit.com", "tiktok.com", "www.tiktok.com": target, err = validateSupportedGenericURL(parsed, host) default: - err = errors.New("This site is not yet supported by FramePier.") + err = errors.New("Only approved YouTube, Twitch, Dailymotion, Vimeo, Reddit and TikTok video URLs are supported.") } if err != nil { return mediaTarget{}, err @@ -271,13 +134,13 @@ func validateParsedDailymotionURL(parsed *url.URL) (mediaTarget, error) { return mediaTarget{}, errors.New("This Dailymotion page is not a supported public video.") } -// This is an explicitly constrained first batch, not an arbitrary-URL pass-through. -// It does not create a network sandbox: yt-dlp's approved extractor may still -// contact media/CDN hosts. Broader site support requires a separate network gate. +// This is a constrained set of additional sites, not arbitrary URL passthrough. +// The named extractors are not network isolation; cross-host fetches and DNS +// remain subject to independent security review before release. func validateSupportedGenericURL(parsed *url.URL, host string) (mediaTarget, error) { if parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Opaque != "" || parsed.ForceQuery || parsed.RawPath != "" || parsed.EscapedPath() != parsed.Path { - return mediaTarget{}, errors.New("Use the direct public video URL without extra parameters.") + return mediaTarget{}, errors.New("Use a direct public video URL without extra parameters.") } var extractor string switch host { @@ -289,7 +152,7 @@ func validateSupportedGenericURL(parsed *url.URL, host string) (mediaTarget, err if tiktokVideoPattern.MatchString(parsed.Path) { extractor = "TikTok" } } if extractor == "" { - return mediaTarget{}, errors.New("Open a single public video page on this site, not a feed or profile.") + return mediaTarget{}, errors.New("Open a single public video page, not a feed or profile.") } return mediaTarget{CanonicalURL: parsed.String(), Source: sourceGeneric, Extractor: extractor}, nil } From c1064de8deabe757a99c7bede04779037e5d26b4 Mon Sep 17 00:00:00 2001 From: Dafka <322898297+dafka007@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:47:29 +0300 Subject: [PATCH 5/6] style: apply gofmt formatting to universal-site validation --- helper/src/url_validation.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/helper/src/url_validation.go b/helper/src/url_validation.go index 2b13bbb..430da9a 100644 --- a/helper/src/url_validation.go +++ b/helper/src/url_validation.go @@ -145,11 +145,17 @@ func validateSupportedGenericURL(parsed *url.URL, host string) (mediaTarget, err var extractor string switch host { case "vimeo.com", "www.vimeo.com": - if vimeoVideoPattern.MatchString(parsed.Path) { extractor = "Vimeo" } + if vimeoVideoPattern.MatchString(parsed.Path) { + extractor = "Vimeo" + } case "reddit.com", "www.reddit.com": - if redditVideoPattern.MatchString(parsed.Path) { extractor = "Reddit" } + if redditVideoPattern.MatchString(parsed.Path) { + extractor = "Reddit" + } case "tiktok.com", "www.tiktok.com": - if tiktokVideoPattern.MatchString(parsed.Path) { extractor = "TikTok" } + if tiktokVideoPattern.MatchString(parsed.Path) { + extractor = "TikTok" + } } if extractor == "" { return mediaTarget{}, errors.New("Open a single public video page, not a feed or profile.") From 4e755410ef764d7f8efead979aee13d50e26ac41 Mon Sep 17 00:00:00 2001 From: Dafka <322898297+dafka007@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:47:32 +0300 Subject: [PATCH 6/6] style: apply gofmt formatting to universal-site contract tests --- helper/src/universal_site_contract_test.go | 106 ++++++++++----------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/helper/src/universal_site_contract_test.go b/helper/src/universal_site_contract_test.go index 83d8ee1..123a0ae 100644 --- a/helper/src/universal_site_contract_test.go +++ b/helper/src/universal_site_contract_test.go @@ -7,63 +7,63 @@ import "testing" // probe rather than being refused solely because it is not one of our three // original integrations. These tests do not perform network requests. func TestUniversalSiteAcceptsPublicHTTPSPages(t *testing.T) { - urls := []string{ - "https://vimeo.com/123456789", - "https://www.reddit.com/r/videos/comments/abc123/example/", - "https://www.tiktok.com/@creator/video/123456789", - } - for _, raw := range urls { - t.Run(raw, func(t *testing.T) { - got, err := validateMediaURL(raw, "") - if err != nil { - t.Fatalf("public HTTPS page rejected: %v", err) - } - if got.Source != mediaSource("generic") { - t.Fatalf("source = %q; want generic", got.Source) - } - if got.CanonicalURL != raw { - t.Fatalf("canonical URL = %q; want %q", got.CanonicalURL, raw) - } - // Generic sites have no trusted site-specific ID until yt-dlp - // returns validated metadata. - if got.ID != "" { - t.Fatalf("generic ID must not be guessed: %q", got.ID) - } - }) - } + urls := []string{ + "https://vimeo.com/123456789", + "https://www.reddit.com/r/videos/comments/abc123/example/", + "https://www.tiktok.com/@creator/video/123456789", + } + for _, raw := range urls { + t.Run(raw, func(t *testing.T) { + got, err := validateMediaURL(raw, "") + if err != nil { + t.Fatalf("public HTTPS page rejected: %v", err) + } + if got.Source != mediaSource("generic") { + t.Fatalf("source = %q; want generic", got.Source) + } + if got.CanonicalURL != raw { + t.Fatalf("canonical URL = %q; want %q", got.CanonicalURL, raw) + } + // Generic sites have no trusted site-specific ID until yt-dlp + // returns validated metadata. + if got.ID != "" { + t.Fatalf("generic ID must not be guessed: %q", got.ID) + } + }) + } } func TestUniversalSiteRejectsUnsafeEntryURLs(t *testing.T) { - cases := []string{ - "http://vimeo.com/123456789", - "https://localhost/watch", - "https://nas.local/watch", - "https://intranet/watch", - "https://metadata.google.internal/latest/meta-data/", - "https://127.0.0.1/watch", - "https://10.0.0.1/watch", - "https://172.16.1.1/watch", - "https://192.168.1.1/watch", - "https://169.254.169.254/latest/meta-data/", - "https://[::1]/watch", - "https://[fc00::1]/watch", - "https://[fe80::1]/watch", - "https://user:password@vimeo.com/123456789", - "https://vimeo.com:8443/123456789", - "file:///C:/Windows/System32/", - "https://vimeo.com/\nInjected: header", - } - for _, raw := range cases { - t.Run(raw, func(t *testing.T) { - if got, err := validateMediaURL(raw, ""); err == nil { - t.Fatalf("unsafe entry URL accepted: %#v", got) - } - }) - } + cases := []string{ + "http://vimeo.com/123456789", + "https://localhost/watch", + "https://nas.local/watch", + "https://intranet/watch", + "https://metadata.google.internal/latest/meta-data/", + "https://127.0.0.1/watch", + "https://10.0.0.1/watch", + "https://172.16.1.1/watch", + "https://192.168.1.1/watch", + "https://169.254.169.254/latest/meta-data/", + "https://[::1]/watch", + "https://[fc00::1]/watch", + "https://[fe80::1]/watch", + "https://user:password@vimeo.com/123456789", + "https://vimeo.com:8443/123456789", + "file:///C:/Windows/System32/", + "https://vimeo.com/\nInjected: header", + } + for _, raw := range cases { + t.Run(raw, func(t *testing.T) { + if got, err := validateMediaURL(raw, ""); err == nil { + t.Fatalf("unsafe entry URL accepted: %#v", got) + } + }) + } } func TestUniversalSiteDoesNotTrustArbitrarySuppliedVideoID(t *testing.T) { - if got, err := validateMediaURL("https://vimeo.com/123456789", "forged-id"); err == nil { - t.Fatalf("untrusted supplied ID accepted: %#v", got) - } + if got, err := validateMediaURL("https://vimeo.com/123456789", "forged-id"); err == nil { + t.Fatalf("untrusted supplied ID accepted: %#v", got) + } }