Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions helper/src/universal_site_contract_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
40 changes: 39 additions & 1 deletion helper/src/url_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ 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 string // validated extractor for bounded additional sites
}

var (
Expand All @@ -30,6 +32,9 @@ var (
// 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) {
Expand All @@ -49,8 +54,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("Only approved YouTube, Twitch, Dailymotion, Vimeo, Reddit and TikTok video URLs are supported.")
}
if err != nil {
return mediaTarget{}, err
Expand Down Expand Up @@ -127,6 +134,35 @@ func validateParsedDailymotionURL(parsed *url.URL) (mediaTarget, error) {
return mediaTarget{}, errors.New("This Dailymotion page is not a supported public video.")
}

// 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 a 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, 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 == "" {
Expand Down Expand Up @@ -154,6 +190,8 @@ func sourceLabel(source mediaSource) string {
return "Twitch VOD"
case sourceDailymotion:
return "Dailymotion"
case sourceGeneric:
return "Supported site"
default:
return "YouTube"
}
Expand Down
4 changes: 4 additions & 0 deletions helper/src/ytdlp_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}

Expand Down