mirror of
https://github.com/iv-org/invidious.git
synced 2026-09-07 17:42:46 -05:00
fix(companion): handle caption cache edge cases
This commit is contained in:
parent
aeae6d90bd
commit
93ab8c4a0a
@ -149,6 +149,26 @@ Spectator.describe Invidious::SubtitleCache do
|
|||||||
expect(cache.get("k2")).to be_nil
|
expect(cache.get("k2")).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "rejects entries when entry capacity is zero" do
|
||||||
|
cache = Invidious::SubtitleCache.new(max_entries: 0)
|
||||||
|
vtt = "WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nZero capacity"
|
||||||
|
|
||||||
|
expect(cache.put("k", vtt, "text/vtt")).to be_nil
|
||||||
|
expect(cache.size).to eq(0)
|
||||||
|
|
||||||
|
byte_limited = Invidious::SubtitleCache.new(max_bytes: 0)
|
||||||
|
expect(byte_limited.put("k", vtt, "text/vtt")).to be_nil
|
||||||
|
expect(byte_limited.size).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "matches exact caption proxy paths only" do
|
||||||
|
regex = Invidious::SubtitleCache::CAPTION_PATH_REGEX
|
||||||
|
|
||||||
|
expect(regex.match("/api/v1/captions/video_id").try &.[1]).to eq("video_id")
|
||||||
|
expect(regex.match("/companion/api/v1/captions/video_id").try &.[1]).to eq("video_id")
|
||||||
|
expect(regex.match("/api/v1/captions/video_id/extra")).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
it "evicts oldest entries when max_bytes is exceeded" do
|
it "evicts oldest entries when max_bytes is exceeded" do
|
||||||
cache = Invidious::SubtitleCache.new(max_entries: 10, max_bytes: 60)
|
cache = Invidious::SubtitleCache.new(max_entries: 10, max_bytes: 60)
|
||||||
vtt1 = "WEBVTT\n\n12345678901234567890" # ~30 bytes
|
vtt1 = "WEBVTT\n\n12345678901234567890" # ~30 bytes
|
||||||
@ -195,10 +215,14 @@ Spectator.describe Invidious::SubtitleCache do
|
|||||||
|
|
||||||
it "stops reading bodies beyond the entry size limit" do
|
it "stops reading bodies beyond the entry size limit" do
|
||||||
valid_body = "WEBVTT\n"
|
valid_body = "WEBVTT\n"
|
||||||
expect(Invidious::SubtitleCache.read_limited_body(IO::Memory.new(valid_body))).to eq(valid_body)
|
valid_result = Invidious::SubtitleCache.read_limited_body(IO::Memory.new(valid_body))
|
||||||
|
expect(valid_result.oversized).to be_false
|
||||||
|
expect(valid_result.body).to eq(valid_body)
|
||||||
|
|
||||||
oversized_body = "x" * (Invidious::SubtitleCache::MAX_ENTRY_BYTES + 1)
|
oversized_body = "x" * (Invidious::SubtitleCache::MAX_ENTRY_BYTES + 1)
|
||||||
expect(Invidious::SubtitleCache.read_limited_body(IO::Memory.new(oversized_body))).to be_nil
|
oversized_result = Invidious::SubtitleCache.read_limited_body(IO::Memory.new(oversized_body))
|
||||||
|
expect(oversized_result.oversized).to be_true
|
||||||
|
expect(oversized_result.body).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
module Invidious
|
module Invidious
|
||||||
class SubtitleCache
|
class SubtitleCache
|
||||||
|
CAPTION_PATH_REGEX = %r{^/(?:companion/)?api/v1/captions/([^/?#]+)$}
|
||||||
|
|
||||||
struct Entry
|
struct Entry
|
||||||
getter body : String
|
getter body : String
|
||||||
getter content_type : String
|
getter content_type : String
|
||||||
@ -33,6 +35,14 @@ module Invidious
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct LimitedBody
|
||||||
|
getter body : String?
|
||||||
|
getter oversized : Bool
|
||||||
|
|
||||||
|
def initialize(@body : String?, @oversized : Bool)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
DEFAULT_MAX_ENTRIES = 256
|
DEFAULT_MAX_ENTRIES = 256
|
||||||
DEFAULT_MAX_BYTES = 64 * 1024 * 1024 # 64 MiB
|
DEFAULT_MAX_BYTES = 64 * 1024 * 1024 # 64 MiB
|
||||||
DEFAULT_TTL = 6.hours # 21600 seconds
|
DEFAULT_TTL = 6.hours # 21600 seconds
|
||||||
@ -62,7 +72,7 @@ module Invidious
|
|||||||
header_end == trimmed.size || " \t\r\n".includes?(trimmed[header_end])
|
header_end == trimmed.size || " \t\r\n".includes?(trimmed[header_end])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.read_limited_body(input : IO, limit : Int32 = MAX_ENTRY_BYTES) : String?
|
def self.read_limited_body(input : IO, limit : Int32 = MAX_ENTRY_BYTES) : LimitedBody
|
||||||
output = IO::Memory.new
|
output = IO::Memory.new
|
||||||
buffer = Bytes.new(READ_CHUNK_BYTES)
|
buffer = Bytes.new(READ_CHUNK_BYTES)
|
||||||
|
|
||||||
@ -74,8 +84,8 @@ module Invidious
|
|||||||
output.write(buffer[0, bytes_read])
|
output.write(buffer[0, bytes_read])
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil if input.read_byte
|
oversized = !input.read_byte.nil?
|
||||||
output.to_s
|
LimitedBody.new(oversized ? nil : output.to_s, oversized)
|
||||||
end
|
end
|
||||||
|
|
||||||
def size : Int32
|
def size : Int32
|
||||||
@ -120,6 +130,7 @@ module Invidious
|
|||||||
|
|
||||||
private def put_internal(key : String, body : String, content_type : String) : Entry?
|
private def put_internal(key : String, body : String, content_type : String) : Entry?
|
||||||
return nil unless SubtitleCache.valid_vtt?(body)
|
return nil unless SubtitleCache.valid_vtt?(body)
|
||||||
|
return nil if @max_entries <= 0 || @max_bytes <= 0
|
||||||
return nil if body.bytesize > MAX_ENTRY_BYTES || body.bytesize > @max_bytes
|
return nil if body.bytesize > MAX_ENTRY_BYTES || body.bytesize > @max_bytes
|
||||||
|
|
||||||
if old = @entries.delete(key)
|
if old = @entries.delete(key)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ module Invidious::Routes::Companion
|
|||||||
end
|
end
|
||||||
|
|
||||||
path = env.request.path.rstrip('/')
|
path = env.request.path.rstrip('/')
|
||||||
if match = path.match(%r{^/(?:companion/)?api/v1/captions/([^/?#]+)})
|
if match = path.match(Invidious::SubtitleCache::CAPTION_PATH_REGEX)
|
||||||
video_id = match[1]
|
video_id = match[1]
|
||||||
return self.handle_caption_request(env, url, video_id)
|
return self.handle_caption_request(env, url, video_id)
|
||||||
end
|
end
|
||||||
@ -107,7 +107,13 @@ module Invidious::Routes::Companion
|
|||||||
private def self.fetch_from_companion(url : String, headers : HTTP::Headers) : Invidious::SubtitleCache::Response?
|
private def self.fetch_from_companion(url : String, headers : HTTP::Headers) : Invidious::SubtitleCache::Response?
|
||||||
COMPANION_POOL.client do |wrapper|
|
COMPANION_POOL.client do |wrapper|
|
||||||
wrapper.client.get(url, headers) do |resp|
|
wrapper.client.get(url, headers) do |resp|
|
||||||
body = Invidious::SubtitleCache.read_limited_body(resp.body_io)
|
limited_body = Invidious::SubtitleCache.read_limited_body(resp.body_io)
|
||||||
|
if limited_body.oversized
|
||||||
|
resp.body_io.close
|
||||||
|
return oversized_caption_response
|
||||||
|
end
|
||||||
|
|
||||||
|
body = limited_body.body
|
||||||
return nil unless body
|
return nil unless body
|
||||||
response_headers = HTTP::Headers.new
|
response_headers = HTTP::Headers.new
|
||||||
resp.headers.each do |key, value|
|
resp.headers.each do |key, value|
|
||||||
@ -120,6 +126,14 @@ module Invidious::Routes::Companion
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def self.oversized_caption_response
|
||||||
|
headers = HTTP::Headers.new
|
||||||
|
headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
headers["Cache-Control"] = "no-store"
|
||||||
|
headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||||
|
Invidious::SubtitleCache::Response.new(413, headers, "Caption response exceeds size limit\n")
|
||||||
|
end
|
||||||
|
|
||||||
private def self.proxy_companion(env, response)
|
private def self.proxy_companion(env, response)
|
||||||
env.response.status_code = response.status_code
|
env.response.status_code = response.status_code
|
||||||
response.headers.each do |key, value|
|
response.headers.each do |key, value|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user