Skip to content

Commit 2f335b3

Browse files
committed
Use a dedicated endpoind for downloads
This allows us to not pass file name ("title") in the form data and to enforce some sanity checks
1 parent fe057c7 commit 2f335b3

File tree

5 files changed

+82
-31
lines changed

5 files changed

+82
-31
lines changed

src/invidious.cr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ before_all do |env|
236236
"/api/manifest/",
237237
"/videoplayback",
238238
"/latest_version",
239+
"/download",
239240
}.any? { |r| env.request.resource.starts_with? r }
240241

241242
if env.request.cookies.has_key? "SID"
@@ -348,6 +349,8 @@ end
348349
Invidious::Routing.get "/e/:id", Invidious::Routes::Watch, :redirect
349350
Invidious::Routing.get "/redirect", Invidious::Routes::Misc, :cross_instance_redirect
350351

352+
Invidious::Routing.post "/download", Invidious::Routes::Watch, :download
353+
351354
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
352355
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
353356

src/invidious/frontend/watch_page.cr

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ module Invidious::Frontend::WatchPage
2626
return String.build(4000) do |str|
2727
str << "<form"
2828
str << " class=\"pure-form pure-form-stacked\""
29-
str << " action='/latest_version'"
30-
str << " method='get'"
29+
str << " action='/download'"
30+
str << " method='post'"
3131
str << " rel='noopener'"
3232
str << " target='_blank'>"
3333
str << '\n'
3434

35+
# Hidden inputs for video id and title
36+
str << "<input type='hidden' name='id' value='" << video.id << "'/>\n"
37+
str << "<input type='hidden' name='title' value='" << HTML.escape(video.title) << "'/>\n"
38+
3539
str << "\t<div class=\"pure-control-group\">\n"
3640

3741
str << "\t\t<label for='download_widget'>"
@@ -48,8 +52,7 @@ module Invidious::Frontend::WatchPage
4852

4953
height = itag_to_metadata?(option["itag"]).try &.["height"]?
5054

51-
title = URI.encode_www_form("#{video.title}-#{video.id}.#{mimetype.split("/")[1]}")
52-
value = {"id": video.id, "itag": option["itag"], "title": title}.to_json
55+
value = {"itag": option["itag"], "ext": mimetype.split("/")[1]}.to_json
5356

5457
str << "\t\t\t<option value='" << value << "'>"
5558
str << (height || "~240") << "p - " << mimetype
@@ -61,8 +64,7 @@ module Invidious::Frontend::WatchPage
6164
video_assets.video_streams.each do |option|
6265
mimetype = option["mimeType"].as_s.split(";")[0]
6366

64-
title = URI.encode_www_form("#{video.title}-#{video.id}.#{mimetype.split("/")[1]}")
65-
value = {"id": video.id, "itag": option["itag"], "title": title}.to_json
67+
value = {"itag": option["itag"], "ext": mimetype.split("/")[1]}.to_json
6668

6769
str << "\t\t\t<option value='" << value << "'>"
6870
str << option["qualityLabel"] << " - " << mimetype << " @ " << option["fps"] << "fps - video only"
@@ -74,8 +76,7 @@ module Invidious::Frontend::WatchPage
7476
video_assets.audio_streams.each do |option|
7577
mimetype = option["mimeType"].as_s.split(";")[0]
7678

77-
title = URI.encode_www_form("#{video.title}-#{video.id}.#{mimetype.split("/")[1]}")
78-
value = {"id": video.id, "itag": option["itag"], "title": title}.to_json
79+
value = {"itag": option["itag"], "ext": mimetype.split("/")[1]}.to_json
7980

8081
str << "\t\t\t<option value='" << value << "'>"
8182
str << mimetype << " @ " << (option["bitrate"]?.try &.as_i./ 1000) << "k - audio only"
@@ -85,8 +86,7 @@ module Invidious::Frontend::WatchPage
8586
# Subtitles (a.k.a "closed captions")
8687

8788
video_assets.captions.each do |caption|
88-
title = URI.encode_www_form("#{video.title}-#{video.id}.#{caption.language_code}.vtt")
89-
value = {"id": video.id, "label": caption.name, "title": title}.to_json
89+
value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json
9090

9191
str << "\t\t\t<option value='" << value << "'>"
9292
str << translate(locale, "download_subtitles", translate(locale, caption.name))

src/invidious/routes/api/v1/videos.cr

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ module Invidious::Routes::API::V1::Videos
2323
env.response.content_type = "application/json"
2424

2525
id = env.params.url["id"]
26-
region = env.params.query["region"]?
26+
region = env.params.query["region"]? || env.params.body["region"]?
27+
28+
if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/)
29+
return error_json(400, "Invalid video ID")
30+
end
2731

2832
# See https://github.com/ytdl-org/youtube-dl/blob/6ab30ff50bf6bd0585927cb73c7421bef184f87a/youtube_dl/extractor/youtube.py#L1354
2933
# It is possible to use `/api/timedtext?type=list&v=#{id}` and

src/invidious/routes/video_playback.cr

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -242,31 +242,25 @@ module Invidious::Routes::VideoPlayback
242242
# YouTube /videoplayback links expire after 6 hours,
243243
# so we have a mechanism here to redirect to the latest version
244244
def self.latest_version(env)
245-
if env.params.query["download_widget"]?
246-
download_widget = JSON.parse(env.params.query["download_widget"])
245+
id = env.params.query["id"]?
246+
itag = env.params.query["itag"]?.try &.to_i?
247247

248-
id = download_widget["id"].as_s
249-
title = URI.decode_www_form(download_widget["title"].as_s)
250-
251-
if label = download_widget["label"]?
252-
return env.redirect "/api/v1/captions/#{id}?label=#{label}&title=#{title}"
253-
else
254-
itag = download_widget["itag"].as_s.to_i
255-
local = "true"
256-
end
248+
# Sanity checks
249+
if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/)
250+
return error_template(400, "Invalid video ID")
257251
end
258252

259-
id ||= env.params.query["id"]?
260-
itag ||= env.params.query["itag"]?.try &.to_i
253+
if itag.nil? || itag <= 0 || itag >= 1000
254+
return error_template(400, "Invalid itag")
255+
end
261256

262257
region = env.params.query["region"]?
258+
local = (env.params.query["local"]? == "true")
263259

264-
local ||= env.params.query["local"]?
265-
local ||= "false"
266-
local = local == "true"
260+
title = env.params.query["title"]?
267261

268-
if !id || !itag
269-
haltf env, status_code: 400, response: "TESTING"
262+
if title && CONFIG.disabled?("downloads")
263+
return error_template(403, "Administrator has disabled this endpoint.")
270264
end
271265

272266
video = get_video(id, region: region)
@@ -278,8 +272,10 @@ module Invidious::Routes::VideoPlayback
278272
haltf env, status_code: 404
279273
end
280274

281-
url = URI.parse(url).request_target.not_nil! if local
282-
url = "#{url}&title=#{title}" if title
275+
if local
276+
url = URI.parse(url).request_target.not_nil!
277+
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
278+
end
283279

284280
return env.redirect url
285281
end

src/invidious/routes/watch.cr

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,52 @@ module Invidious::Routes::Watch
289289
return error_template(404, "The requested clip doesn't exist")
290290
end
291291
end
292+
293+
def self.download(env)
294+
if CONFIG.disabled?("downloads")
295+
return error_template(403, "Administrator has disabled this endpoint.")
296+
end
297+
298+
title = env.params.body["title"]? || ""
299+
video_id = env.params.body["id"]? || ""
300+
selection = env.params.body["download_widget"]?
301+
302+
if title.empty? || video_id.empty? || selection.nil?
303+
return error_template(400, "Missing form data")
304+
end
305+
306+
download_widget = JSON.parse(selection)
307+
extension = download_widget["ext"].as_s
308+
309+
filename = URI.encode_www_form(
310+
"#{video_id}-#{title}.#{extension}",
311+
space_to_plus: false
312+
)
313+
314+
# Pass form parameters as URL parameters for the handlers of both
315+
# /latest_version and /api/v1/captions. This avoids an un-necessary
316+
# redirect and duplicated (and hazardous) sanity checks.
317+
env.params.query["id"] = video_id
318+
env.params.query["title"] = filename
319+
320+
# Delete the useless ones
321+
env.params.body.delete("id")
322+
env.params.body.delete("title")
323+
env.params.body.delete("download_widget")
324+
325+
if label = download_widget["label"]?
326+
# URL params specific to /api/v1/captions/:id
327+
env.params.query["label"] = URI.encode_www_form(label.as_s, space_to_plus: false)
328+
329+
return Invidious::Routes::API::V1::Videos.captions(env)
330+
elsif itag = download_widget["itag"]?.try &.as_i
331+
# URL params specific to /latest_version
332+
env.params.query["itag"] = itag.to_s
333+
env.params.query["local"] = "true"
334+
335+
return Invidious::Routes::VideoPlayback.latest_version(env)
336+
else
337+
return error_template(400, "Invalid label or itag")
338+
end
339+
end
292340
end

0 commit comments

Comments
 (0)