diff options
| author | Samantaz Fox <coding@samantaz.fr> | 2022-02-22 18:11:11 +0100 |
|---|---|---|
| committer | Samantaz Fox <coding@samantaz.fr> | 2022-02-23 13:00:30 +0100 |
| commit | 2f335b3d2c2805d5de1b0204920c439b87f5646b (patch) | |
| tree | 5445181e1452f2d07cc4a818ec283ba13c6a800b /src | |
| parent | fe057c78737458132248e39b7ee7572b67f26918 (diff) | |
| download | invidious-2f335b3d2c2805d5de1b0204920c439b87f5646b.tar.gz invidious-2f335b3d2c2805d5de1b0204920c439b87f5646b.tar.bz2 invidious-2f335b3d2c2805d5de1b0204920c439b87f5646b.zip | |
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
Diffstat (limited to 'src')
| -rw-r--r-- | src/invidious.cr | 3 | ||||
| -rw-r--r-- | src/invidious/frontend/watch_page.cr | 20 | ||||
| -rw-r--r-- | src/invidious/routes/api/v1/videos.cr | 6 | ||||
| -rw-r--r-- | src/invidious/routes/video_playback.cr | 36 | ||||
| -rw-r--r-- | src/invidious/routes/watch.cr | 48 |
5 files changed, 82 insertions, 31 deletions
diff --git a/src/invidious.cr b/src/invidious.cr index d742cd59..d1c3ac83 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -236,6 +236,7 @@ before_all do |env| "/api/manifest/", "/videoplayback", "/latest_version", + "/download", }.any? { |r| env.request.resource.starts_with? r } if env.request.cookies.has_key? "SID" @@ -348,6 +349,8 @@ end Invidious::Routing.get "/e/:id", Invidious::Routes::Watch, :redirect Invidious::Routing.get "/redirect", Invidious::Routes::Misc, :cross_instance_redirect + Invidious::Routing.post "/download", Invidious::Routes::Watch, :download + Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index d3a50705..80b67641 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -26,12 +26,16 @@ module Invidious::Frontend::WatchPage return String.build(4000) do |str| str << "<form" str << " class=\"pure-form pure-form-stacked\"" - str << " action='/latest_version'" - str << " method='get'" + str << " action='/download'" + str << " method='post'" str << " rel='noopener'" str << " target='_blank'>" str << '\n' + # Hidden inputs for video id and title + str << "<input type='hidden' name='id' value='" << video.id << "'/>\n" + str << "<input type='hidden' name='title' value='" << HTML.escape(video.title) << "'/>\n" + str << "\t<div class=\"pure-control-group\">\n" str << "\t\t<label for='download_widget'>" @@ -48,8 +52,7 @@ module Invidious::Frontend::WatchPage height = itag_to_metadata?(option["itag"]).try &.["height"]? - title = URI.encode_www_form("#{video.title}-#{video.id}.#{mimetype.split("/")[1]}") - value = {"id": video.id, "itag": option["itag"], "title": title}.to_json + value = {"itag": option["itag"], "ext": mimetype.split("/")[1]}.to_json str << "\t\t\t<option value='" << value << "'>" str << (height || "~240") << "p - " << mimetype @@ -61,8 +64,7 @@ module Invidious::Frontend::WatchPage video_assets.video_streams.each do |option| mimetype = option["mimeType"].as_s.split(";")[0] - title = URI.encode_www_form("#{video.title}-#{video.id}.#{mimetype.split("/")[1]}") - value = {"id": video.id, "itag": option["itag"], "title": title}.to_json + value = {"itag": option["itag"], "ext": mimetype.split("/")[1]}.to_json str << "\t\t\t<option value='" << value << "'>" str << option["qualityLabel"] << " - " << mimetype << " @ " << option["fps"] << "fps - video only" @@ -74,8 +76,7 @@ module Invidious::Frontend::WatchPage video_assets.audio_streams.each do |option| mimetype = option["mimeType"].as_s.split(";")[0] - title = URI.encode_www_form("#{video.title}-#{video.id}.#{mimetype.split("/")[1]}") - value = {"id": video.id, "itag": option["itag"], "title": title}.to_json + value = {"itag": option["itag"], "ext": mimetype.split("/")[1]}.to_json str << "\t\t\t<option value='" << value << "'>" str << mimetype << " @ " << (option["bitrate"]?.try &.as_i./ 1000) << "k - audio only" @@ -85,8 +86,7 @@ module Invidious::Frontend::WatchPage # Subtitles (a.k.a "closed captions") video_assets.captions.each do |caption| - title = URI.encode_www_form("#{video.title}-#{video.id}.#{caption.language_code}.vtt") - value = {"id": video.id, "label": caption.name, "title": title}.to_json + value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json str << "\t\t\t<option value='" << value << "'>" str << translate(locale, "download_subtitles", translate(locale, caption.name)) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 2b23d2ad..8bca6930 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -23,7 +23,11 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "application/json" id = env.params.url["id"] - region = env.params.query["region"]? + region = env.params.query["region"]? || env.params.body["region"]? + + if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/) + return error_json(400, "Invalid video ID") + end # See https://github.com/ytdl-org/youtube-dl/blob/6ab30ff50bf6bd0585927cb73c7421bef184f87a/youtube_dl/extractor/youtube.py#L1354 # It is possible to use `/api/timedtext?type=list&v=#{id}` and diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 6ac1e780..df243945 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -242,31 +242,25 @@ module Invidious::Routes::VideoPlayback # YouTube /videoplayback links expire after 6 hours, # so we have a mechanism here to redirect to the latest version def self.latest_version(env) - if env.params.query["download_widget"]? - download_widget = JSON.parse(env.params.query["download_widget"]) + id = env.params.query["id"]? + itag = env.params.query["itag"]?.try &.to_i? - id = download_widget["id"].as_s - title = URI.decode_www_form(download_widget["title"].as_s) - - if label = download_widget["label"]? - return env.redirect "/api/v1/captions/#{id}?label=#{label}&title=#{title}" - else - itag = download_widget["itag"].as_s.to_i - local = "true" - end + # Sanity checks + if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/) + return error_template(400, "Invalid video ID") end - id ||= env.params.query["id"]? - itag ||= env.params.query["itag"]?.try &.to_i + if itag.nil? || itag <= 0 || itag >= 1000 + return error_template(400, "Invalid itag") + end region = env.params.query["region"]? + local = (env.params.query["local"]? == "true") - local ||= env.params.query["local"]? - local ||= "false" - local = local == "true" + title = env.params.query["title"]? - if !id || !itag - haltf env, status_code: 400, response: "TESTING" + if title && CONFIG.disabled?("downloads") + return error_template(403, "Administrator has disabled this endpoint.") end video = get_video(id, region: region) @@ -278,8 +272,10 @@ module Invidious::Routes::VideoPlayback haltf env, status_code: 404 end - url = URI.parse(url).request_target.not_nil! if local - url = "#{url}&title=#{title}" if title + if local + url = URI.parse(url).request_target.not_nil! + url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title + end return env.redirect url end diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index c34ce715..7688d2a8 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -289,4 +289,52 @@ module Invidious::Routes::Watch return error_template(404, "The requested clip doesn't exist") end end + + def self.download(env) + if CONFIG.disabled?("downloads") + return error_template(403, "Administrator has disabled this endpoint.") + end + + title = env.params.body["title"]? || "" + video_id = env.params.body["id"]? || "" + selection = env.params.body["download_widget"]? + + if title.empty? || video_id.empty? || selection.nil? + return error_template(400, "Missing form data") + end + + download_widget = JSON.parse(selection) + extension = download_widget["ext"].as_s + + filename = URI.encode_www_form( + "#{video_id}-#{title}.#{extension}", + space_to_plus: false + ) + + # Pass form parameters as URL parameters for the handlers of both + # /latest_version and /api/v1/captions. This avoids an un-necessary + # redirect and duplicated (and hazardous) sanity checks. + env.params.query["id"] = video_id + env.params.query["title"] = filename + + # Delete the useless ones + env.params.body.delete("id") + env.params.body.delete("title") + env.params.body.delete("download_widget") + + if label = download_widget["label"]? + # URL params specific to /api/v1/captions/:id + env.params.query["label"] = URI.encode_www_form(label.as_s, space_to_plus: false) + + return Invidious::Routes::API::V1::Videos.captions(env) + elsif itag = download_widget["itag"]?.try &.as_i + # URL params specific to /latest_version + env.params.query["itag"] = itag.to_s + env.params.query["local"] = "true" + + return Invidious::Routes::VideoPlayback.latest_version(env) + else + return error_template(400, "Invalid label or itag") + end + end end |
