diff --git a/config/config.example.yml b/config/config.example.yml index 8d3e6212..6a2a71f8 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -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)://:" ## Default: ## #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 diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 4d69854c..e47e405c 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -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 diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index b5269668..63b935ec 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -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" diff --git a/src/invidious/routes/companion.cr b/src/invidious/routes/companion.cr new file mode 100644 index 00000000..bcfbad6b --- /dev/null +++ b/src/invidious/routes/companion.cr @@ -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 diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr index 930e4915..f04302a2 100644 --- a/src/invidious/routes/embed.cr +++ b/src/invidious/routes/embed.cr @@ -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" diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index e777b3f1..8a4fa246 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -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" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 46b71f1f..a51bb4b6 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -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 # ------------------- diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 0daed46c..42241d15 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -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 diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index b40092a1..44861c37 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -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 ####################################################################