Add Invidious check ID for storyboards (used for video storyboards in the video timeline)

The storyboards API endpoint is unprotected and it allows anyone,
  including bots and abusers to spam that endpoint without having to
  enter the `/watch` endpoint (most of the time, is protected by some
  sort of bot protection by current instance owners to prevent abuse)

  I wonder if there is a better way to do this, but this works fine
This commit is contained in:
Fijxu 2026-01-19 01:46:04 -03:00
parent 66c67f4c7a
commit c28be6720f
8 changed files with 55 additions and 9 deletions

View File

@ -434,7 +434,7 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') {
} }
player.vttThumbnails({ player.vttThumbnails({
src: '/api/v1/storyboards/' + video_data.id + '?height=90', src: '/api/v1/storyboards/' + video_data.id + '?height=90' + `${video_data.invidious_companion_check_id ? `&check=${video_data.invidious_companion_check_id}` : ""}`,
showTimestamp: true showTimestamp: true
}); });

View File

@ -89,6 +89,16 @@ db:
## ##
#invidious_companion_key: "CHANGE_ME!!" #invidious_companion_key: "CHANGE_ME!!"
##
## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion.
## The key needs to be exactly 16 characters long.
##
## Accepted values: true, false
## Default: true
##
#invidious_companion_verify_requests: true
######################################### #########################################
# #
# Server config # Server config

View File

@ -173,6 +173,9 @@ class Config
# Invidious companion API key # Invidious companion API key
property invidious_companion_key : String = "" property invidious_companion_key : String = ""
# Verify requests on endpoints that use Invidious companion
property invidious_companion_verify_requests : Bool = true
# Saved cookies in "name1=value1; name2=value2..." format # Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)] @[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new property cookies : HTTP::Cookies = HTTP::Cookies.new

View File

@ -384,9 +384,13 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
return text return text
end end
def encrypt_ecb_without_salt(data, key) def ecb_without_salt(data, key, encrypt : Bool)
cipher = OpenSSL::Cipher.new("aes-128-ecb") cipher = OpenSSL::Cipher.new("aes-128-ecb")
if encrypt
cipher.encrypt cipher.encrypt
else
cipher.decrypt
end
cipher.key = key cipher.key = key
io = IO::Memory.new io = IO::Memory.new
@ -394,11 +398,25 @@ def encrypt_ecb_without_salt(data, key)
io.write(cipher.final) io.write(cipher.final)
io.rewind io.rewind
if encrypt
return io return io
else
return io.gets_to_end
end
end end
def invidious_companion_encrypt(data) def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key) encrypted_data = ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key, encrypt: true)
return Base64.urlsafe_encode(encrypted_data) return Base64.urlsafe_encode(encrypted_data)
end end
def invidious_companion_decrypt(check_id)
check_id_decoded = Base64.decode_string(check_id)
begin
decrypted_data = ecb_without_salt(check_id_decoded, CONFIG.invidious_companion_key, encrypt: false)
rescue
return nil
end
return decrypted_data.as(String).split("|")
end

View File

@ -181,6 +181,18 @@ module Invidious::Routes::API::V1::Videos
id = env.params.url["id"] id = env.params.url["id"]
region = env.params.query["region"]? region = env.params.query["region"]?
if CONFIG.invidious_companion.present? && CONFIG.invidious_companion_verify_requests
invidious_companion_check_id = env.params.query["check"]?
if check_id = invidious_companion_check_id
video_id = invidious_companion_decrypt(check_id).try &.[1]
if id != video_id
haltf env, 401, "ID incorrect."
end
else
haltf env, 401, "No check ID."
end
end
begin begin
video = get_video(id, region: region) video = get_video(id, region: region)
rescue ex : NotFoundException rescue ex : NotFoundException

View File

@ -1,6 +1,3 @@
<%
invidious_companion_check_id = invidious_companion_encrypt(video.id) if invidious_companion
%>
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>" <video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
id="player" class="on-video_player video-js player-style-<%= params.player_style %>" id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
preload="<% if params.preload %>auto<% else %>none<% end %>" preload="<% if params.preload %>auto<% else %>none<% end %>"

View File

@ -1,3 +1,7 @@
<%
invidious_companion_check_id = invidious_companion_encrypt(video.id) if invidious_companion
%>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="<%= preferences.locale %>"> <html lang="<%= preferences.locale %>">

View File

@ -1,6 +1,7 @@
<% 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) %>
<% invidious_companion_check_id = invidious_companion_encrypt(video.id) if invidious_companion %>
<% content_for "header" do %> <% content_for "header" do %>
@ -66,7 +67,8 @@ we're going to need to do it here in order to allow for translations.
"projection_type" => video.projection_type, "projection_type" => video.projection_type,
"local_disabled" => CONFIG.disabled?("local"), "local_disabled" => CONFIG.disabled?("local"),
"support_reddit" => true, "support_reddit" => true,
"live_now" => video.live_now "live_now" => video.live_now,
"invidious_companion_check_id" => invidious_companion_check_id,
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>