1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
|
module Invidious::Routes::VideoPlayback
# /videoplayback
def self.get_video_playback(env)
locale = env.get("preferences").as(Preferences).locale
query_params = env.params.query
fvip = query_params["fvip"]? || "3"
mns = query_params["mn"]?.try &.split(",")
mns ||= [] of String
if query_params["region"]?
region = query_params["region"]
query_params.delete("region")
end
if query_params["host"]? && !query_params["host"].empty?
host = query_params["host"]
query_params.delete("host")
else
host = "r#{fvip}---#{mns.pop}.googlevideo.com"
end
# Sanity check, to avoid being used as an open proxy
if !host.matches?(/[\w-]+.googlevideo.com/)
return error_template(400, "Invalid \"host\" parameter.")
end
host = "https://#{host}"
url = "/videoplayback?#{query_params}"
headers = HTTP::Headers.new
REQUEST_HEADERS_WHITELIST.each do |header|
if env.request.headers[header]?
headers[header] = env.request.headers[header]
end
end
# See: https://github.com/iv-org/invidious/issues/3302
range_header = env.request.headers["Range"]?
if range_header.nil?
range_for_head = query_params["range"]? || "0-640"
headers["Range"] = "bytes=#{range_for_head}"
end
client = make_client(URI.parse(host), region, force_resolve: true)
response = HTTP::Client::Response.new(500)
error = ""
5.times do
begin
response = client.head(url, headers)
if response.headers["Location"]?
location = URI.parse(response.headers["Location"])
env.response.headers["Access-Control-Allow-Origin"] = "*"
new_host = "#{location.scheme}://#{location.host}"
if new_host != host
host = new_host
client.close
client = make_client(URI.parse(new_host), region, force_resolve: true)
end
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
else
break
end
rescue Socket::Addrinfo::Error
if !mns.empty?
mn = mns.pop
end
fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region, force_resolve: true)
rescue ex
error = ex.message
end
end
# Remove the Range header added previously.
headers.delete("Range") if range_header.nil?
playback_statistics = get_playback_statistic()
playback_statistics["totalRequests"] += 1
if response.status_code >= 400
env.response.content_type = "text/plain"
haltf env, response.status_code
else
playback_statistics["successfulRequests"] += 1
end
if url.includes? "&file=seg.ts"
if CONFIG.disabled?("livestreams")
return error_template(403, "Administrator has disabled this endpoint.")
end
begin
client.get(url, headers) do |resp|
resp.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = resp.headers["Location"]?
url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
return env.redirect url
end
IO.copy(resp.body_io, env.response)
end
rescue ex
end
else
if query_params["title"]? && CONFIG.disabled?("downloads") ||
CONFIG.disabled?("dash")
return error_template(403, "Administrator has disabled this endpoint.")
end
content_length = nil
first_chunk = true
range_start, range_end = parse_range(env.request.headers["Range"]?)
chunk_start = range_start
chunk_end = range_end
if !chunk_end || chunk_end - chunk_start > HTTP_CHUNK_SIZE
chunk_end = chunk_start + HTTP_CHUNK_SIZE - 1
end
# TODO: Record bytes written so we can restart after a chunk fails
loop do
if !range_end && content_length
range_end = content_length
end
if range_end && chunk_start > range_end
break
end
if range_end && chunk_end > range_end
chunk_end = range_end
end
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
begin
client.get(url, headers) do |resp|
if first_chunk
if !env.request.headers["Range"]? && resp.status_code == 206
env.response.status_code = 200
else
env.response.status_code = resp.status_code
end
resp.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) && key.downcase != "content-range"
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = resp.headers["Location"]?
location = URI.parse(location)
location = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
env.redirect location
break
end
if title = query_params["title"]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
filename = URI.encode_www_form(title, space_to_plus: false)
header = "attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}"
env.response.headers["Content-Disposition"] = header
end
if !resp.headers.includes_word?("Transfer-Encoding", "chunked")
content_length = resp.headers["Content-Range"].split("/")[-1].to_i64
if env.request.headers["Range"]?
env.response.headers["Content-Range"] = "bytes #{range_start}-#{range_end || (content_length - 1)}/#{content_length}"
env.response.content_length = ((range_end.try &.+ 1) || content_length) - range_start
else
env.response.content_length = content_length
end
end
end
proxy_file(resp, env)
end
rescue ex
if ex.message != "Error reading socket: Connection reset by peer"
break
else
client.close
client = make_client(URI.parse(host), region, force_resolve: true)
end
end
chunk_start = chunk_end + 1
chunk_end += HTTP_CHUNK_SIZE
first_chunk = false
end
end
client.close
end
# /videoplayback/*
def self.get_video_playback_greedy(env)
path = env.request.path
path = path.lchop("/videoplayback/")
path = path.rchop("/")
path = path.gsub(/mime\/\w+\/\w+/) do |mimetype|
mimetype = mimetype.split("/")
mimetype[0] + "/" + mimetype[1] + "%2F" + mimetype[2]
end
path = path.split("/")
raw_params = {} of String => Array(String)
path.each_slice(2) do |pair|
key, value = pair
value = URI.decode_www_form(value)
if raw_params[key]?
raw_params[key] << value
else
raw_params[key] = [value]
end
end
query_params = HTTP::Params.new(raw_params)
env.response.headers["Access-Control-Allow-Origin"] = "*"
return env.redirect "/videoplayback?#{query_params}"
end
# /videoplayback/* && /videoplayback/*
def self.options_video_playback(env)
env.response.headers.delete("Content-Type")
env.response.headers["Access-Control-Allow-Origin"] = "*"
env.response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, Range"
end
# /latest_version
#
# YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version
def self.latest_version(env)
id = env.params.query["id"]?
itag = env.params.query["itag"]?.try &.to_i?
# Sanity checks
if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/)
return error_template(400, "Invalid video ID")
end
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")
title = env.params.query["title"]?
if title && CONFIG.disabled?("downloads")
return error_template(403, "Administrator has disabled this endpoint.")
end
begin
video = get_video(id, region: region)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex
return error_template(500, ex)
end
if itag.nil?
fmt = video.fmt_stream[-1]?
else
fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag }
end
url = fmt.try &.["url"]?.try &.as_s
if !url
haltf env, status_code: 404
end
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
end
|