Compare commits

...

7 commits

Author SHA1 Message Date
a77a272935
fix missing scheme on meta elements
All checks were successful
Invidious CI / build (push) Successful in 5m7s
2025-04-03 03:12:37 -03:00
ce052103e7
deprecate support for external video playback proxy 2025-04-03 03:12:36 -03:00
c57a4f4920
add CSP based on backend selected by the user
xd
2025-04-03 03:12:36 -03:00
426e7bfbdb
use Host header on img-src 'self' data: CSP 2025-04-03 03:12:36 -03:00
3d85519ec9
Only show embed link on error pages if v query param is present 2025-04-03 03:12:36 -03:00
a74d89b6d9
Safely handle missing current_companion just in case 2025-04-03 03:12:36 -03:00
fd8c40e0da
fix: fix wrong invidious companion logic on backends
do not change to another companion if request fails
2025-04-03 03:12:36 -03:00
15 changed files with 55 additions and 143 deletions

View file

@ -37,16 +37,6 @@ https://git.nadeko.net/Fijxu/-/packages/container/invidious/latest
- [Removal of materialized views on PostgreSQL](github.com/iv-org/invidious/pull/2469): If you don't have this on your Invidious public instance, your SSD will suffer and it will catch on fire https://github.com/iv-org/invidious/pull/2469#issuecomment-2012623454
- External video playback proxy: Let's you use an external video playback proxy like https://git.nadeko.net/Fijxu/http3-ytproxy or https://github.com/TeamPiped/piped-proxy instead of the one that is bundled with Invidious. It's useful if you are proxying video and your throughput is not low. I did this to distribute the traffic across different servers. If you are selfhosting only for a few amount of people, this is not really useful for you.
It can be set using this on `config.yml`:
```yaml
external_videoplayback_proxy: "https://inv-proxy.example.com"
```
> [!NOTE]
> If you setup this, Invidious will check if the proxy is alive doing a request to `https://inv-proxy.example.com/health`, and if it doesn't get a response code of 200, Invidious will fallback to the local videoplayback proxy! This is only currently supported by https://git.nadeko.net/Fijxu/http3-ytproxy
- Limit the DASH resolution sent to the clients: It can be set using `max_dash_resolution` on the config. Example: `max_dash_resolution: 1080`
- [Limit requests made to Youtube API when pulling subscriptions (feeds)](https://git.nadeko.net/Fijxu/invidious/commit/df94f1c0b82d95846574487231ea251530838ef0): Due to the recent changes of Youtube ("This helps protect out community", "Sign in to confirm you are not a bot"), subscriptions now have limited information, this is because Invidious by default, makes a video request to youtube to be able to get more information about the video, like `length_seconds`, `live_now`, `premiere_timestamp`, and `views`. If you have a lot of users with a ton of subscriptions, Invidious will basically spam youtube API all the time, resulting in a block from youtube.

View file

@ -113,9 +113,11 @@ YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
COMPANION_POOL = CompanionConnectionPool.new(
capacity: CONFIG.pool_size
)
COMPANION_POOL = [] of CompanionConnectionPool
CONFIG.invidious_companion.each do |companion|
COMPANION_POOL << CompanionConnectionPool.new(companion, capacity: CONFIG.pool_size)
end
# CLI
Kemal.config.extra_options do |parser|
@ -208,14 +210,6 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
else
# Invidious will it's own videoplayback proxy unless the admin decides to rewrite
# the /videoplayback location in the reverse proxy configuration (NGINX, Caddy, etc)
LOGGER.info("jobs: Disabling CheckExternalProxy job. Invidious will it's own videoplayback proxy")
end
if !CONFIG.tokens_server.empty?
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
else

View file

@ -215,10 +215,6 @@ class Config
# of the backend
property backends_delimiter : String = "|"
# External videoplayback proxies list. They should include `https://`
# at the start of the URI
property external_videoplayback_proxy : Array(String) = [] of String
property pubsub_domain : String = ""
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"

View file

@ -2,12 +2,11 @@ module BackendInfo
extend self
@@exvpp_url : Array(String) = Array.new(CONFIG.invidious_companion.size, "")
@@status : Array(Int32) = Array.new(CONFIG.invidious_companion.size, 0)
@@extra_media_csp : String = ""
@@extra_connect_csp : String = ""
@@csp : Array(String) = Array.new(CONFIG.invidious_companion.size, "")
@@mutex : Mutex = Mutex.new
def check_backends
check_companion()
generate_csp()
end
private def check_companion
@ -17,6 +16,7 @@ module BackendInfo
response = HTTP::Client.get "#{companion.private_url}/healthz"
if response.status_code == 200
check_videoplayback_proxy(companion, index)
generate_csp(companion.public_url.to_s, @@exvpp_url[index], index)
else
@@status[index] = 0
end
@ -41,7 +41,7 @@ module BackendInfo
exvpp_health = HTTP::Client.get "#{exvpp_url}/health"
if exvpp_health.status_code == 200
@@status[index] = 2
return
return exvpp_url
else
@@status[index] = 1
end
@ -53,20 +53,9 @@ module BackendInfo
end
end
private def generate_csp
@@extra_media_csp = ""
@@extra_connect_csp = ""
CONFIG.invidious_companion.each do |companion|
@@extra_media_csp += " #{companion.public_url}"
@@extra_connect_csp += " #{companion.public_url}"
end
exvpp_urls = self.get_exvpp
exvpp_urls.each do |exvpp_url|
if !exvpp_url.empty?
@@extra_media_csp += " #{exvpp_url}"
@@extra_connect_csp += " #{exvpp_url}"
end
private def generate_csp(companion_url : String, exvpp_url : String, index : Int32)
@@mutex.synchronize do
@@csp[index] = " #{companion_url} #{exvpp_url}"
end
end
@ -78,7 +67,12 @@ module BackendInfo
return @@exvpp_url
end
def get_csp
return @@extra_media_csp, @@extra_connect_csp
def get_csp(index : Int32)
# A little mutex to prevent sending a partial CSP header
# Not sure if this is necessary. But if the @@csp[index] is being assigned
# at the same time when it's being accessed, a data race will appear
@@mutex.synchronize do
return @@csp[index], @@csp[index]
end
end
end

View file

@ -183,6 +183,8 @@ def error_redirect_helper(env : HTTP::Server::Context)
go_to_youtube_embed = translate(locale, "videoinfo_youTube_embed_link")
switch_instance = translate(locale, "Switch Invidious Instance")
show_embed_link = "(<a rel=\"noreferrer noopener\" href=\"https://youtube.com/embed/#{env.params.query["v"]}\">#{go_to_youtube_embed}</a>)" if env.params.query["v"]?
return <<-END_HTML
<p style="margin-bottom: 4px;">#{next_steps_text}</p>
<ul>
@ -194,7 +196,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
</li>
<li>
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
(<a rel="noreferrer noopener" href="https://youtube.com/embed/#{env.params.query["v"]}">#{go_to_youtube_embed}</a>)
#{show_embed_link}
</li>
</ul>
END_HTML

View file

@ -4,30 +4,6 @@ module Invidious::HttpServer
module Utils
extend self
@@proxy_alive : String = ""
def check_external_proxy
CONFIG.external_videoplayback_proxy.each do |proxy|
begin
response = HTTP::Client.get("#{proxy}/health")
if response.status_code == 200
@@proxy_alive = proxy
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy}'")
break
end
rescue
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy}' is not available")
end
end
if @@proxy_alive.empty?
LOGGER.warn("CheckExternalProxy: No proxies alive! Using own server proxy")
end
end
def get_external_proxy
return @@proxy_alive
end
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
url = URI.parse(raw_url)
@ -38,11 +14,7 @@ module Invidious::HttpServer
url.query_params = params
if absolute
if !@@proxy_alive.empty?
return "#{@@proxy_alive}#{url.request_target}"
else
return "#{HOST_URL}#{url.request_target}"
end
return "#{HOST_URL}#{url.request_target}"
else
return url.request_target
end

View file

@ -1,13 +0,0 @@
class Invidious::Jobs::CheckExternalProxy < Invidious::Jobs::BaseJob
def initialize
end
def begin
loop do
HttpServer::Utils.check_external_proxy
LOGGER.info("CheckExternalProxy: Done, sleeping for 10 seconds")
sleep 10.seconds
Fiber.yield
end
end
end

View file

@ -222,19 +222,13 @@ module Invidious::Routes::API::Manifest
raw_params["host"] = uri.host.not_nil!
proxy = Invidious::HttpServer::Utils.get_external_proxy
if CONFIG.https_only
scheme = "https://"
else
scheme = "http://"
end
if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}"
else
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end
end

View file

@ -51,14 +51,7 @@ module Invidious::Routes::BeforeAll
env.set "current_companion", current_companion
end
extra_media_csp, extra_connect_csp = BackendInfo.get_csp
end
if !CONFIG.external_videoplayback_proxy.empty?
CONFIG.external_videoplayback_proxy.each do |proxy|
extra_media_csp += " #{proxy}"
extra_connect_csp += " #{proxy}"
end
extra_media_csp, extra_connect_csp = BackendInfo.get_csp(env.get("current_companion").as(Int32))
end
# Allow media resources to be loaded from google servers
@ -76,11 +69,12 @@ module Invidious::Routes::BeforeAll
# TODO: Remove style-src's 'unsafe-inline', requires to remove all
# inline styles (<style> [..] </style>, style=" [..] ")
scheme = env.request.headers["X-Forwarded-Proto"]? || ("https" if CONFIG.https_only) || "http"
env.response.headers["Content-Security-Policy"] = {
"default-src 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: " + HOST_URL,
"img-src 'self' data: " + "#{scheme}://#{env.request.headers["Host"]?}",
"font-src 'self' data:",
"connect-src 'self'" + extra_connect_csp,
"manifest-src 'self'",

View file

@ -313,16 +313,7 @@ module Invidious::Routes::VideoPlayback
end
if local
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
if !external_proxy.empty?
url = URI.parse(url)
external_proxy = URI.parse(external_proxy)
url.host = external_proxy.host
url.port = external_proxy.port
url = url.to_s
else
url = URI.parse(url).request_target.not_nil!
end
url = URI.parse(url).request_target.not_nil!
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
end

View file

@ -3,6 +3,7 @@
author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
host = env.request.headers["Host"]
scheme = env.request.headers["X-Forwarded-Proto"]? || ("https" if CONFIG.https_only) || "http"
relative_url =
case selected_tab
@ -32,15 +33,15 @@
<%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= host %>/channel/<%= ucid %>">
<meta property="og:url" content="<%= scheme %>://<%= host %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<meta property="og:image" content="<%= scheme %>://<%= host %>/ggpht<%= channel_profile_pic %>">
<meta property="og:description" content="<%= channel.description %>">
<meta name="twitter:card" content="summary">
<meta name="twitter:url" content="<%= host %>/channel/<%= ucid %>">
<meta name="twitter:url" content="<%= scheme %>://<%= host %>/channel/<%= ucid %>">
<meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<meta name="twitter:image" content="<%= scheme %>://<%= host %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%>

View file

@ -1,7 +1,6 @@
<%
locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
%>
<!DOCTYPE html>
<html lang="<%= locale %>">
@ -109,7 +108,7 @@
<%
if CONFIG.invidious_companion.present?
current_backend = env.get("current_companion").as(Int32)
current_backend = env.get?("current_companion").try &.as(Int32)
domain = env.get?("domain").try &.as(Bool)
status = BackendInfo.get_status
%>
@ -333,12 +332,9 @@
<div class="footer-footer">
<%
if CONFIG.invidious_companion.present?
current_backend = env.get("current_companion").as(Int32)
current_backend = env.get?("current_companion").try &.as(Int32)
%>
<div class="box">You are currently using Backend: <%= CONFIG.invidious_companion[current_backend].public_url %></div>
<% end %>
<% if !current_external_videoplayback_proxy.empty? %>
<div class="box">External Videoplayback Proxy: <%= current_external_videoplayback_proxy %></div>
<div class="box">You are currently using Backend: <%= current_backend ? CONFIG.invidious_companion[current_backend].public_url : "Unable to get backend, this is bug, please report it!" %></div>
<% end %>
<span class="left">
<% if CONFIG.modified_source_code_url %>

View file

@ -2,31 +2,32 @@
<% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %>
<% scheme = env.request.headers["X-Forwarded-Proto"]? || ("https" if CONFIG.https_only) || "http" %>
<% content_for "header" do %>
<meta name="thumbnail" content="<%= thumbnail %>">
<meta name="thumbnail" content="<%= scheme %>://<%= host %><%= thumbnail %>">
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
<meta name="keywords" content="<%= video.keywords.join(",") %>">
<meta property="og:site_name" content="<%= author %> | Invidious">
<meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta property="og:url" content="<%= scheme %>://<%= host %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:image" content="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
<meta property="og:type" content="video.other">
<!-- This shouldn't be empty, ever. -->
<meta property="og:video" content="<%= video_url %>">
<meta property="og:video" content="<%= video_url %>">
<meta property="og:video:url" content="<%= video_url %>">
<meta property="og:video:secure_url" content="<%= video_url %>">
<meta property="og:video:type" content="video/mp4">
<meta property="og:video:width" content="640">
<meta property="og:video:height" content="360">
<meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta name="twitter:url" content="<%= scheme %>://<%= host %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>">
<meta name="twitter:image" content="<%= scheme %>://<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= scheme %>://<%= host %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">

View file

@ -49,7 +49,7 @@ end
struct CompanionConnectionPool
property pool : DB::Pool(HTTP::Client)
def initialize(capacity = 5, timeout = 5.0)
def initialize(companion, capacity = 5, timeout = 5.0)
options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: capacity,
@ -58,12 +58,11 @@ struct CompanionConnectionPool
)
@pool = DB::Pool(HTTP::Client).new(options) do
companion = CONFIG.invidious_companion.sample
next make_client(companion.private_url, use_http_proxy: false)
end
end
def client(env : HTTP::Server::Context | Nil, &)
def client(&)
conn = pool.checkout
begin
@ -71,14 +70,10 @@ struct CompanionConnectionPool
rescue ex
conn.close
if env.nil?
companion = CONFIG.invidious_companion.sample
else
current_companion = env.get("current_companion").as(Int32)
companion = CONFIG.invidious_companion[current_companion]
end
scheme = "https" if conn.tls || "http"
same_companion = "#{scheme}://#{conn.host}:#{conn.port}"
conn = make_client(companion.private_url, use_http_proxy: false)
conn = make_client(URI.parse(same_companion), use_http_proxy: false)
response = yield conn
ensure

View file

@ -688,7 +688,12 @@ module YoutubeAPI
# Send the POST request
begin
response = COMPANION_POOL.client(env, &.post(endpoint, headers: headers, body: data.to_json))
if env.nil?
current_companion = rand(CONFIG.invidious_companion.size)
else
current_companion = env.get("current_companion").as(Int32)
end
response = COMPANION_POOL[current_companion].client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body
if (response.status_code != 200)
raise Exception.new(