This commit is contained in:
Émilien (perso) 2025-06-14 20:35:31 +00:00 committed by GitHub
commit bc7ae1763a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 138 additions and 33 deletions

View File

@ -75,17 +75,25 @@ db:
## If you are using a reverse proxy then you will probably need to
## configure the public_url to be the same as the domain used for Invidious.
## Also apply when used from an external IP address (without a domain).
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
## Examples: https://MYINVIDIOUSDOMAIN/companion or http://192.168.1.100:8282/companion
##
## Both parameter can have identical URL when Invidious is hosted in
## an internal network or at home or locally (localhost).
##
## NOTE: If public_url is omitted, Invidious will use its built-in proxy
## to route companion requests through /companion, which is useful for
## simple setups where companion runs on the same network. When using
## the built-in proxy, CSP headers are not modified since requests
## stay within the same domain.
##
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
## Default: <none>
##
#invidious_companion:
# - private_url: "http://localhost:8282"
# public_url: "http://localhost:8282"
# - private_url: "http://localhost:8282/companion"
# public_url: "http://localhost:8282/companion"
# # Example with built-in proxy (omit public_url):
# # - private_url: "http://localhost:8282/companion"
##
## API key for Invidious companion, used for securing the communication

View File

@ -82,6 +82,9 @@ class Config
@[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("")
# Indicates if this companion instance uses the built-in proxy
property builtin_proxy : Bool = false
end
# Number of threads to use for crawling videos from channels (for updating subscriptions)
@ -271,6 +274,14 @@ class Config
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
exit(1)
end
# Set public_url to built-in proxy path when omitted
config.invidious_companion.each do |companion|
if companion.public_url.to_s.empty?
companion.public_url = URI.parse("/companion")
companion.builtin_proxy = true
end
end
elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else

View File

@ -63,6 +63,7 @@ module Invidious::Routes::BeforeAll
"/videoplayback",
"/latest_version",
"/download",
"/companion/",
}.any? { |r| env.request.resource.starts_with? r }
if env.request.cookies.has_key? "SID"

View File

@ -0,0 +1,44 @@
module Invidious::Routes::Companion
# /companion
def self.get_companion(env)
url = env.request.path
if env.request.query
url += "?#{env.request.query}"
end
begin
COMPANION_POOL.client do |wrapper|
puts env.request.headers
wrapper.client.get(url, env.request.headers) do |resp|
return self.proxy_companion(env, resp)
end
end
rescue ex
end
end
def self.options_companion(env)
url = env.request.path
if env.request.query
url += "?#{env.request.query}"
end
begin
COMPANION_POOL.client do |wrapper|
wrapper.client.options(url, env.request.headers) do |resp|
return self.proxy_companion(env, resp)
end
end
rescue ex
end
end
private def self.proxy_companion(env, response)
env.response.status_code = response.status_code
response.headers.each do |key, value|
env.response.headers[key] = value
end
return IO.copy response.body_io, env.response
end
end

View File

@ -209,10 +209,17 @@ module Invidious::Routes::Embed
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
invidious_companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
uri =
"#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
end.join(" ")
if !invidious_companion_urls.empty?
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion_urls}")
.gsub("connect-src", "connect-src #{invidious_companion_urls}")
end
end
rendered "embed"

View File

@ -194,10 +194,17 @@ module Invidious::Routes::Watch
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion.public_url}")
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
invidious_companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
uri =
"#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
end.join(" ")
if !invidious_companion_urls.empty?
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion_urls}")
.gsub("connect-src", "connect-src #{invidious_companion_urls}")
end
end
templated "watch"

View File

@ -46,6 +46,7 @@ module Invidious::Routing
self.register_api_v1_routes
self.register_api_manifest_routes
self.register_video_playback_routes
self.register_companion_routes
end
# -------------------
@ -188,7 +189,7 @@ module Invidious::Routing
end
# -------------------
# Media proxy routes
# Proxy routes
# -------------------
def register_api_manifest_routes
@ -223,6 +224,13 @@ module Invidious::Routing
get "/vi/:id/:name", Routes::Images, :thumbnails
end
def register_companion_routes
if CONFIG.invidious_companion.present?
get "/companion/*", Routes::Companion, :get_companion
options "/companion/*", Routes::Companion, :options_companion
end
end
# -------------------
# API routes
# -------------------

View File

@ -46,8 +46,27 @@ struct YoutubeConnectionPool
end
end
# Packages a `HTTP::Client` to an Invidious companion instance alongside the configuration for that instance.
#
# This is used as the resource for the `CompanionPool` as to allow the ability to
# proxy the requests to Invidious companion from Invidious directly.
# Instead of setting up routes in a reverse proxy.
struct CompanionWrapper
property client : HTTP::Client
property companion : Config::CompanionConfig
def initialize(companion : Config::CompanionConfig)
@companion = companion
@client = make_client(companion.private_url, use_http_proxy: false)
end
def close
@client.close
end
end
struct CompanionConnectionPool
property pool : DB::Pool(HTTP::Client)
property pool : DB::Pool(CompanionWrapper)
def initialize(capacity = 5, timeout = 5.0)
options = DB::Pool::Options.new(
@ -57,26 +76,28 @@ struct CompanionConnectionPool
checkout_timeout: timeout
)
@pool = DB::Pool(HTTP::Client).new(options) do
@pool = DB::Pool(CompanionWrapper).new(options) do
companion = CONFIG.invidious_companion.sample
next make_client(companion.private_url, use_http_proxy: false)
make_client(companion.private_url, use_http_proxy: false)
CompanionWrapper.new(companion: companion)
end
end
def client(&)
conn = pool.checkout
wrapper = pool.checkout
begin
response = yield conn
response = yield wrapper
rescue ex
conn.close
wrapper.close
companion = CONFIG.invidious_companion.sample
conn = make_client(companion.private_url, use_http_proxy: false)
make_client(companion.private_url, use_http_proxy: false)
wrapper = CompanionWrapper.new(companion: companion)
response = yield conn
response = yield wrapper
ensure
pool.release(conn)
pool.release(wrapper)
end
response

View File

@ -695,22 +695,20 @@ module YoutubeAPI
# Send the POST request
begin
response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body
if (response.status_code != 200)
raise Exception.new(
"Error while communicating with Invidious companion: \
status code: #{response.status_code} and body: #{body.dump}"
)
response_body = Hash(String, JSON::Any).new
COMPANION_POOL.client do |wrapper|
companion_base_url = wrapper.companion.private_url.path
wrapper.client.post("#{companion_base_url}#{endpoint}", headers: headers, body: data.to_json) do |response|
response_body = JSON.parse(response.body_io).as_h
end
end
return response_body
rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end
# Convert result to Hash
initial_data = JSON.parse(body).as_h
return initial_data
end
####################################################################