diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f925e2c9..79095374 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2169,12 +2169,33 @@ func TestYouTubeApiKey(t *testing.T) { } } +func TestDefaultYouTubeEmbedUrl(t *testing.T) { + os.Clearenv() + + opts, err := NewParser().ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + expected := "https://www.youtube-nocookie.com/embed/" + result := opts.YouTubeEmbedUrlOverride() + + if result != expected { + t.Fatalf(`Unexpected default value, got %v instead of %v`, result, expected) + } + + expected = "www.youtube-nocookie.com" + result = opts.YouTubeEmbedDomain() + if result != expected { + t.Fatalf(`Unexpected YouTube embed domain, got %v instead of %v`, result, expected) + } +} + func TestYouTubeEmbedUrlOverride(t *testing.T) { os.Clearenv() os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() + opts, err := NewParser().ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) } @@ -2185,6 +2206,12 @@ func TestYouTubeEmbedUrlOverride(t *testing.T) { if result != expected { t.Fatalf(`Unexpected YOUTUBE_EMBED_URL_OVERRIDE value, got %v instead of %v`, result, expected) } + + expected = "invidious.custom" + result = opts.YouTubeEmbedDomain() + if result != expected { + t.Fatalf(`Unexpected YouTube embed domain, got %v instead of %v`, result, expected) + } } func TestParseConfigDumpOutput(t *testing.T) { diff --git a/internal/config/options.go b/internal/config/options.go index 9605dfe3..636592c1 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -155,6 +155,7 @@ type Options struct { filterEntryMaxAgeDays int youTubeApiKey string youTubeEmbedUrlOverride string + youTubeEmbedDomain string oauth2UserCreationAllowed bool oauth2ClientID string oauth2ClientSecret string @@ -521,11 +522,19 @@ func (o *Options) YouTubeApiKey() string { return o.youTubeApiKey } -// YouTubeEmbedUrlOverride returns YouTube URL which will be used for embeds +// YouTubeEmbedUrlOverride returns the YouTube embed URL override if defined. func (o *Options) YouTubeEmbedUrlOverride() string { return o.youTubeEmbedUrlOverride } +// YouTubeEmbedDomain returns the domain used for YouTube embeds. +func (o *Options) YouTubeEmbedDomain() string { + if o.youTubeEmbedDomain != "" { + return o.youTubeEmbedDomain + } + return "www.youtube-nocookie.com" +} + // FetchNebulaWatchTime returns true if the Nebula video duration // should be fetched and used as a reading time. func (o *Options) FetchNebulaWatchTime() bool { diff --git a/internal/config/parser.go b/internal/config/parser.go index 29ef5018..27b55120 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -298,6 +298,13 @@ func (p *Parser) parseLines(lines []string) (err error) { if port != "" { p.opts.listenAddr = ":" + port } + + youtubeEmbedURL, err := url.Parse(p.opts.youTubeEmbedUrlOverride) + if err != nil { + return fmt.Errorf("config: invalid YOUTUBE_EMBED_URL_OVERRIDE value: %w", err) + } + p.opts.youTubeEmbedDomain = youtubeEmbedURL.Hostname() + return nil } diff --git a/internal/reader/sanitizer/sanitizer.go b/internal/reader/sanitizer/sanitizer.go index 6e9f879e..911aee93 100644 --- a/internal/reader/sanitizer/sanitizer.go +++ b/internal/reader/sanitizer/sanitizer.go @@ -110,6 +110,21 @@ var ( "munderover": {}, "semantics": {}, } + + iframeAllowList = map[string]struct{}{ + "bandcamp.com": {}, + "cdn.embedly.com": {}, + "dailymotion.com": {}, + "open.spotify.com": {}, + "player.bilibili.com": {}, + "player.twitch.tv": {}, + "player.vimeo.com": {}, + "soundcloud.com": {}, + "vk.com": {}, + "w.soundcloud.com": {}, + "youtube-nocookie.com": {}, + "youtube.com": {}, + } ) type SanitizerOptions struct { @@ -266,7 +281,7 @@ func sanitizeAttributes(parsedBaseUrl *url.URL, baseURL, tagName string, attribu if isExternalResourceAttribute(attribute.Key) { switch { case tagName == "iframe": - if !isValidIframeSource(parsedBaseUrl, baseURL, attribute.Val) { + if !isValidIframeSource(attribute.Val) { continue } value = rewriteIframeURL(attribute.Val) @@ -448,39 +463,22 @@ func isBlockedResource(src string) bool { }) } -func isValidIframeSource(parsedBaseUrl *url.URL, baseURL, src string) bool { - whitelist := []string{ - "bandcamp.com", - "cdn.embedly.com", - "player.bilibili.com", - "player.twitch.tv", - "player.vimeo.com", - "soundcloud.com", - "vk.com", - "w.soundcloud.com", - "dailymotion.com", - "youtube-nocookie.com", - "youtube.com", - "open.spotify.com", - } - domain := urllib.Domain(src) +func isValidIframeSource(iframeSourceURL string) bool { + iframeSourceDomain := strings.TrimPrefix(urllib.Domain(iframeSourceURL), "www.") - baseDomain := baseURL - if parsedBaseUrl != nil { - baseDomain = parsedBaseUrl.Hostname() - } - - // allow iframe from same origin - if baseDomain == domain { + if _, ok := iframeAllowList[iframeSourceDomain]; ok { return true } - // allow iframe from custom invidious instance - if config.Opts.InvidiousInstance() == domain { + if ytDomain := config.Opts.YouTubeEmbedDomain(); ytDomain != "" && iframeSourceDomain == strings.TrimPrefix(ytDomain, "www.") { return true } - return slices.Contains(whitelist, strings.TrimPrefix(domain, "www.")) + if invidiousInstance := config.Opts.InvidiousInstance(); invidiousInstance != "" && iframeSourceDomain == strings.TrimPrefix(invidiousInstance, "www.") { + return true + } + + return false } func rewriteIframeURL(link string) string { diff --git a/internal/reader/sanitizer/sanitizer_test.go b/internal/reader/sanitizer/sanitizer_test.go index 9ce692e2..efa53ff1 100644 --- a/internal/reader/sanitizer/sanitizer_test.go +++ b/internal/reader/sanitizer/sanitizer_test.go @@ -13,12 +13,6 @@ import ( "miniflux.app/v2/internal/config" ) -func TestMain(m *testing.M) { - config.Opts = config.NewOptions() - exitCode := m.Run() - os.Exit(exitCode) -} - func BenchmarkSanitize(b *testing.B) { var testCases = map[string][]string{ "miniflux_github.html": {"https://github.com/miniflux/v2", ""}, @@ -352,6 +346,8 @@ func TestInvalidNestedTag(t *testing.T) { } func TestInvalidIFrame(t *testing.T) { + config.Opts = config.NewOptions() + input := `` expected := `` output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) @@ -361,7 +357,51 @@ func TestInvalidIFrame(t *testing.T) { } } +func TestSameDomainIFrame(t *testing.T) { + config.Opts = config.NewOptions() + + input := `` + expected := `` + output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) + + if expected != output { + t.Errorf(`Wrong output: %q != %q`, expected, output) + } +} + +func TestInvidiousIFrame(t *testing.T) { + config.Opts = config.NewOptions() + + input := `` + expected := `` + output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) + + if expected != output { + t.Errorf(`Wrong output: %q != %q`, expected, output) + } +} + +func TestCustomYoutubeEmbedURL(t *testing.T) { + os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://www.invidious.custom/embed/") + + defer os.Clearenv() + var err error + if config.Opts, err = config.NewParser().ParseEnvironmentVariables(); err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + input := `` + expected := `` + output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) + + if expected != output { + t.Errorf(`Wrong output: %q != %q`, expected, output) + } +} + func TestIFrameWithChildElements(t *testing.T) { + config.Opts = config.NewOptions() + input := `` expected := `` output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) @@ -750,13 +790,11 @@ func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) { } func TestReplaceYoutubeURLWithCustomURL(t *testing.T) { - os.Clearenv() + defer os.Clearenv() os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - + config.Opts, err = config.NewParser().ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) }