Merge b709941033a961bb2e5666f73635751fa6542bc4 into adcdb8cb92bbf61bac46102eff026593d0bc87b0

This commit is contained in:
Fijxu 2025-02-28 23:56:15 +00:00 committed by GitHub
commit 43fbb5e12c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 84 additions and 35 deletions

View File

@ -110,6 +110,16 @@ db:
## ##
domain: domain:
##
## Alternative domains. You can add other domains if you
## serve invidious on more than one domain, like Tor
## and I2P addresses.
##
## Accepted values: an array of fully qualified domain names (FQDN)
## Default: <none>
##
#alternative_domains: []
## ##
## Tell Invidious that it is behind a proxy that provides only ## Tell Invidious that it is behind a proxy that provides only
## HTTPS, so all links must use the https:// scheme. This ## HTTPS, so all links must use the https:// scheme. This

View File

@ -105,6 +105,8 @@ class Config
property hmac_key : String = "" property hmac_key : String = ""
# Domain to be used for links to resources on the site where an absolute URL is required # Domain to be used for links to resources on the site where an absolute URL is required
property domain : String? property domain : String?
# Alternative domains. You can add other domains, like TOR and I2P addresses
property alternative_domains : Array(String) = [] of String
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key) # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
property use_pubsub_feeds : Bool | Int32 = false property use_pubsub_feeds : Bool | Int32 = false
property popular_enabled : Bool = true property popular_enabled : Bool = true

View File

@ -209,7 +209,7 @@ module Invidious::Routes::API::Manifest
raw_params["host"] = uri.host.not_nil! raw_params["host"] = uri.host.not_nil!
"#{HOST_URL}/videoplayback?#{raw_params}" "#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end end
end end

View File

@ -226,7 +226,7 @@ module Invidious::Routes::API::V1::Authenticated
end end
playlist = create_playlist(title, privacy, user) playlist = create_playlist(title, privacy, user)
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}" env.response.headers["Location"] = "#{env.request.headers["Host"]}/api/v1/auth/playlists/#{playlist.id}"
env.response.status_code = 201 env.response.status_code = 201
{ {
"title" => title, "title" => title,
@ -336,7 +336,7 @@ module Invidious::Routes::API::V1::Authenticated
Invidious::Database::PlaylistVideos.insert(playlist_video) Invidious::Database::PlaylistVideos.insert(playlist_video)
Invidious::Database::Playlists.update_video_added(plid, playlist_video.index) Invidious::Database::Playlists.update_video_added(plid, playlist_video.index)
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" env.response.headers["Location"] = "#{env.request.headers["Host"]}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}"
env.response.status_code = 201 env.response.status_code = 201
JSON.build do |json| JSON.build do |json|

View File

@ -1,7 +1,7 @@
module Invidious::Routes::ErrorRoutes module Invidious::Routes::ErrorRoutes
def self.error_404(env) def self.error_404(env)
# Workaround for #3117 # Workaround for #3117
if HOST_URL.empty? && env.request.path.starts_with?("/v1/storyboards/sb") if env.request.headers["Host"].empty? && env.request.path.starts_with?("/v1/storyboards/sb")
return env.redirect "#{env.request.path[15..]}?#{env.params.query}" return env.redirect "#{env.request.path[15..]}?#{env.params.query}"
end end

View File

@ -199,21 +199,21 @@ module Invidious::Routes::Feeds
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do "xml:lang": "en-US") do
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") xml.element("link", rel: "self", href: "#{env.request.headers["Host"]}#{env.request.resource}")
xml.element("id") { xml.text "yt:channel:#{ucid}" } xml.element("id") { xml.text "yt:channel:#{ucid}" }
xml.element("yt:channelId") { xml.text ucid } xml.element("yt:channelId") { xml.text ucid }
xml.element("title") { author } xml.element("title") { author }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{ucid}") xml.element("link", rel: "alternate", href: "#{env.request.headers["Host"]}/channel/#{ucid}")
xml.element("author") do xml.element("author") do
xml.element("name") { xml.text author } xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" } xml.element("uri") { xml.text "#{env.request.headers["Host"]}/channel/#{ucid}" }
end end
xml.element("image") do xml.element("image") do
xml.element("url") { xml.text "" } xml.element("url") { xml.text "" }
xml.element("title") { xml.text author } xml.element("title") { xml.text author }
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") xml.element("link", rel: "self", href: "#{env.request.headers["Host"]}#{env.request.resource}")
end end
videos.each do |video| videos.each do |video|
@ -255,9 +255,9 @@ module Invidious::Routes::Feeds
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do "xml:lang": "en-US") do
xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions") xml.element("link", "type": "text/html", rel: "alternate", href: "#{env.request.headers["Host"]}/feed/subscriptions")
xml.element("link", "type": "application/atom+xml", rel: "self", xml.element("link", "type": "application/atom+xml", rel: "self",
href: "#{HOST_URL}#{env.request.resource}") href: "#{env.request.headers["Host"]}#{env.request.resource}")
xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) } xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
(notifications + videos).each do |video| (notifications + videos).each do |video|
@ -286,11 +286,11 @@ module Invidious::Routes::Feeds
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
"xml:lang": "en-US") do "xml:lang": "en-US") do
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") xml.element("link", rel: "self", href: "#{env.request.headers["Host"]}#{env.request.resource}")
xml.element("id") { xml.text "iv:playlist:#{plid}" } xml.element("id") { xml.text "iv:playlist:#{plid}" }
xml.element("iv:playlistId") { xml.text plid } xml.element("iv:playlistId") { xml.text plid }
xml.element("title") { xml.text playlist.title } xml.element("title") { xml.text playlist.title }
xml.element("link", rel: "alternate", href: "#{HOST_URL}/playlist?list=#{plid}") xml.element("link", rel: "alternate", href: "#{env.request.headers["Host"]}/playlist?list=#{plid}")
xml.element("author") do xml.element("author") do
xml.element("name") { xml.text playlist.author } xml.element("name") { xml.text playlist.author }
@ -314,7 +314,7 @@ module Invidious::Routes::Feeds
when "url", "href" when "url", "href"
request_target = URI.parse(node[attribute.name]).request_target request_target = URI.parse(node[attribute.name]).request_target
query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : "" query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}" node[attribute.name] = "#{env.request.headers["Host"]}#{request_target}#{query_string_opt}"
else nil # Skip else nil # Skip
end end
end end
@ -323,7 +323,7 @@ module Invidious::Routes::Feeds
document = document.to_xml(options: XML::SaveOptions::NO_DECL) document = document.to_xml(options: XML::SaveOptions::NO_DECL)
document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match| document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
content = "#{HOST_URL}#{URI.parse(match["url"]).request_target}" content = "#{env.request.headers["Host"]}#{URI.parse(match["url"]).request_target}"
document = document.gsub(match[0], "<uri>#{content}</uri>") document = document.gsub(match[0], "<uri>#{content}</uri>")
end end
document document

View File

@ -60,7 +60,13 @@ module Invidious::Routes::Login
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
Invidious::Database::SessionIDs.insert(sid, email) Invidious::Database::SessionIDs.insert(sid, email)
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid) # Checks if there is any alternative domain, like a second domain name,
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end
else else
return error_template(401, "Wrong username or password") return error_template(401, "Wrong username or password")
end end
@ -163,7 +169,13 @@ module Invidious::Routes::Login
view_name = "subscriptions_#{sha256(user.email)}" view_name = "subscriptions_#{sha256(user.email)}"
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid) # Checks if there is any alternative domain, like a second domain name,
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end
if env.request.cookies["PREFS"]? if env.request.cookies["PREFS"]?
user.preferences = env.get("preferences").as(Preferences) user.preferences = env.get("preferences").as(Preferences)

View File

@ -224,7 +224,13 @@ module Invidious::Routes::PreferencesRoute
File.write("config/config.yml", CONFIG.to_yaml) File.write("config/config.yml", CONFIG.to_yaml)
end end
else else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences) # Checks if there is any alternative domain, like a second domain name,
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
end
end end
env.redirect referer env.redirect referer
@ -259,7 +265,13 @@ module Invidious::Routes::PreferencesRoute
preferences.dark_mode = "dark" preferences.dark_mode = "dark"
end end
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences) # Checks if there is any alternative domain, like a second domain name,
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
end
end end
if redirect if redirect

View File

@ -11,8 +11,8 @@ module Invidious::Routes::Search
xml.element("LongName") { xml.text "Invidious Search" } xml.element("LongName") { xml.text "Invidious Search" }
xml.element("Description") { xml.text "Search for videos, channels, and playlists on Invidious" } xml.element("Description") { xml.text "Search for videos, channels, and playlists on Invidious" }
xml.element("InputEncoding") { xml.text "UTF-8" } xml.element("InputEncoding") { xml.text "UTF-8" }
xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{HOST_URL}/favicon.ico" } xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{env.request.headers["Host"]}/favicon.ico" }
xml.element("Url", type: "text/html", method: "get", template: "#{HOST_URL}/search?q={searchTerms}") xml.element("Url", type: "text/html", method: "get", template: "#{env.request.headers["Host"]}/search?q={searchTerms}")
end end
end end
end end

View File

@ -6,17 +6,23 @@ struct Invidious::User
# Note: we use ternary operator because the two variables # Note: we use ternary operator because the two variables
# used in here are not booleans. # used in here are not booleans.
SECURE = (Kemal.config.ssl || CONFIG.https_only) ? true : false @@secure = (Kemal.config.ssl || CONFIG.https_only) ? true : false
# Session ID (SID) cookie # Session ID (SID) cookie
# Parameter "domain" comes from the global config # Parameter "domain" comes from the global config
def sid(domain : String?, sid) : HTTP::Cookie def sid(domain : String?, sid) : HTTP::Cookie
# Not secure if it's being accessed from I2P
# Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p"
@@secure = false
end
return HTTP::Cookie.new( return HTTP::Cookie.new(
name: "SID", name: "SID",
domain: domain, domain: domain,
value: sid, value: sid,
expires: Time.utc + 2.years, expires: Time.utc + 2.years,
secure: SECURE, secure: @@secure,
http_only: true, http_only: true,
samesite: HTTP::Cookie::SameSite::Lax samesite: HTTP::Cookie::SameSite::Lax
) )
@ -25,12 +31,18 @@ struct Invidious::User
# Preferences (PREFS) cookie # Preferences (PREFS) cookie
# Parameter "domain" comes from the global config # Parameter "domain" comes from the global config
def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie
# Not secure if it's being accessed from I2P
# Browsers expect the domain to include https. On I2P there is no HTTPS
if domain.not_nil!.split(".").last == "i2p"
@@secure = false
end
return HTTP::Cookie.new( return HTTP::Cookie.new(
name: "PREFS", name: "PREFS",
domain: domain, domain: domain,
value: URI.encode_www_form(preferences.to_json), value: URI.encode_www_form(preferences.to_json),
expires: Time.utc + 2.years, expires: Time.utc + 2.years,
secure: SECURE, secure: @@secure,
http_only: false, http_only: false,
samesite: HTTP::Cookie::SameSite::Lax samesite: HTTP::Cookie::SameSite::Lax
) )

View File

@ -25,21 +25,22 @@
first_page: continuation.nil?, first_page: continuation.nil?,
params: env.params.query, params: env.params.query,
) )
host = env.request.headers["Host"]
%> %>
<% content_for "header" do %> <% content_for "header" do %>
<%- if selected_tab.videos? -%> <%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>"> <meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious"> <meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>"> <meta property="og:url" content="<%= host %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>"> <meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>"> <meta property="og:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<meta property="og:description" content="<%= channel.description %>"> <meta property="og:description" content="<%= channel.description %>">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:url" content="<%= HOST_URL %>/channel/<%= ucid %>"> <meta name="twitter:url" content="<%= host %>/channel/<%= ucid %>">
<meta name="twitter:title" content="<%= author %>"> <meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>"> <meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>"> <meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%> <%- end -%>

View File

@ -1,29 +1,29 @@
<% ucid = video.ucid %> <% ucid = video.ucid %>
<% title = HTML.escape(video.title) %> <% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %> <% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %>
<% content_for "header" do %> <% content_for "header" do %>
<meta name="thumbnail" content="<%= thumbnail %>"> <meta name="thumbnail" content="<%= thumbnail %>">
<meta name="description" content="<%= HTML.escape(video.short_description) %>"> <meta name="description" content="<%= HTML.escape(video.short_description) %>">
<meta name="keywords" content="<%= video.keywords.join(",") %>"> <meta name="keywords" content="<%= video.keywords.join(",") %>">
<meta property="og:site_name" content="<%= author %> | Invidious"> <meta property="og:site_name" content="<%= author %> | Invidious">
<meta property="og:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>"> <meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>"> <meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg"> <meta property="og:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>"> <meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
<meta property="og:type" content="video.other"> <meta property="og:type" content="video.other">
<meta property="og:video:url" content="<%= HOST_URL %>/embed/<%= video.id %>"> <meta property="og:video:url" content="<%= host %>/embed/<%= video.id %>">
<meta property="og:video:secure_url" content="<%= HOST_URL %>/embed/<%= video.id %>"> <meta property="og:video:secure_url" content="<%= host %>/embed/<%= video.id %>">
<meta property="og:video:type" content="text/html"> <meta property="og:video:type" content="text/html">
<meta property="og:video:width" content="1280"> <meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720"> <meta property="og:video:height" content="720">
<meta name="twitter:card" content="player"> <meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>"> <meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= title %>"> <meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>"> <meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg"> <meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= HOST_URL %>/embed/<%= video.id %>"> <meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280"> <meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720"> <meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>"> <link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">