rill-node 0.0.0-stage → 2026.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +64 -2
- package/binding.gyp +31 -0
- package/index.d.ts +45 -0
- package/index.js +112 -0
- package/native/darwin-arm64/librill.dylib +0 -0
- package/native/darwin-arm64/rill-media +0 -0
- package/native/darwin-arm64/rill_node.node +0 -0
- package/native/darwin-x64/librill.dylib +0 -0
- package/native/darwin-x64/rill-media +0 -0
- package/native/darwin-x64/rill_node.node +0 -0
- package/native/linux-arm64-gnu/librill.so +0 -0
- package/native/linux-arm64-gnu/rill-media +0 -0
- package/native/linux-arm64-gnu/rill_node.node +0 -0
- package/native/linux-x64-gnu/librill.so +0 -0
- package/native/linux-x64-gnu/rill-media +0 -0
- package/native/linux-x64-gnu/rill_node.node +0 -0
- package/native-src/include/rill.h +90 -0
- package/native-src/include/rill_media.h +22 -0
- package/native-src/shard.yml +9 -0
- package/native-src/src/c_api.cr +194 -0
- package/native-src/src/cli.cr +637 -0
- package/native-src/src/rill/c_api/job.cr +336 -0
- package/native-src/src/rill/c_api/memory.cr +32 -0
- package/native-src/src/rill/c_api/status.cr +25 -0
- package/native-src/src/rill/c_api/sync.cr +125 -0
- package/native-src/src/rill/c_api/worker_pool.cr +403 -0
- package/native-src/src/rill/cancellation.cr +29 -0
- package/native-src/src/rill/client.cr +52 -0
- package/native-src/src/rill/cookie_jar.cr +130 -0
- package/native-src/src/rill/dash/manifest.cr +279 -0
- package/native-src/src/rill/dash/parser.cr +227 -0
- package/native-src/src/rill/download_options.cr +96 -0
- package/native-src/src/rill/download_plan.cr +27 -0
- package/native-src/src/rill/download_progress.cr +16 -0
- package/native-src/src/rill/downloader.cr +662 -0
- package/native-src/src/rill/errors.cr +155 -0
- package/native-src/src/rill/extractor.cr +18 -0
- package/native-src/src/rill/extractor_registry.cr +98 -0
- package/native-src/src/rill/extractors/bandcamp.cr +265 -0
- package/native-src/src/rill/extractors/dash.cr +77 -0
- package/native-src/src/rill/extractors/direct_media.cr +73 -0
- package/native-src/src/rill/extractors/generic.cr +441 -0
- package/native-src/src/rill/extractors/hls.cr +202 -0
- package/native-src/src/rill/extractors/internet_archive.cr +180 -0
- package/native-src/src/rill/extractors/m3u.cr +81 -0
- package/native-src/src/rill/extractors/media_ccc_de.cr +219 -0
- package/native-src/src/rill/extractors/mixcloud.cr +495 -0
- package/native-src/src/rill/extractors/peer_tube.cr +226 -0
- package/native-src/src/rill/extractors/rumble.cr +325 -0
- package/native-src/src/rill/extractors/soundcloud.cr +553 -0
- package/native-src/src/rill/extractors/spotify.cr +184 -0
- package/native-src/src/rill/extractors/twitch_clip.cr +273 -0
- package/native-src/src/rill/extractors/youtube.cr +520 -0
- package/native-src/src/rill/format.cr +82 -0
- package/native-src/src/rill/format_selector.cr +143 -0
- package/native-src/src/rill/hls/parser.cr +257 -0
- package/native-src/src/rill/hls/playlist.cr +100 -0
- package/native-src/src/rill/http_client.cr +503 -0
- package/native-src/src/rill/http_url.cr +28 -0
- package/native-src/src/rill/json_value.cr +66 -0
- package/native-src/src/rill/media.cr +151 -0
- package/native-src/src/rill/media_muxer.cr +333 -0
- package/native-src/src/rill/mime_type.cr +58 -0
- package/native-src/src/rill/output_file_name.cr +91 -0
- package/native-src/src/rill/playlist.cr +109 -0
- package/native-src/src/rill/request_options.cr +179 -0
- package/native-src/src/rill/segment_assembly.cr +70 -0
- package/native-src/src/rill/timestamp.cr +34 -0
- package/native-src/src/rill/version.cr +3 -0
- package/native-src/src/rill.cr +47 -0
- package/package.json +48 -4
- package/scripts/build-native.js +46 -0
- package/scripts/install-gyp.js +10 -0
- package/scripts/install.js +19 -0
- package/scripts/lib.js +1 -0
- package/scripts/libdir.js +23 -0
- package/scripts/package-native.js +35 -0
- package/scripts/platform.js +41 -0
- package/scripts/prepack.js +47 -0
- package/src/addon.c +498 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
require "http/client"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
require "./cookie_jar"
|
|
5
|
+
require "./errors"
|
|
6
|
+
require "./request_options"
|
|
7
|
+
|
|
8
|
+
module Rill
|
|
9
|
+
class HTTPClient
|
|
10
|
+
REDIRECT_STATUSES = {
|
|
11
|
+
301, 302, 303, 307, 308,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
RETRY_STATUSES = {
|
|
15
|
+
408, 425, 429, 500, 502, 503, 504,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
SENSITIVE_HEADERS = {
|
|
19
|
+
"authorization", "cookie", "proxy-authorization", "host",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
HEX = "0123456789ABCDEF"
|
|
23
|
+
|
|
24
|
+
class Response
|
|
25
|
+
getter status : HTTP::Status
|
|
26
|
+
getter body : String
|
|
27
|
+
getter headers : HTTP::Headers
|
|
28
|
+
getter url : URI
|
|
29
|
+
|
|
30
|
+
def initialize(
|
|
31
|
+
@status : HTTP::Status,
|
|
32
|
+
@body : String,
|
|
33
|
+
@headers : HTTP::Headers,
|
|
34
|
+
@url : URI,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@cookies : CookieJar
|
|
40
|
+
@header_cookies : Hash(String, String)
|
|
41
|
+
|
|
42
|
+
def initialize(@options : RequestOptions = RequestOptions.new)
|
|
43
|
+
@cookies = CookieJar.new
|
|
44
|
+
@header_cookies = parse_cookie_values(@options.headers.get?("Cookie"))
|
|
45
|
+
validate_options!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
getter cookies : CookieJar
|
|
49
|
+
|
|
50
|
+
def get(url : String | URI, headers : HTTP::Headers? = nil) : Response
|
|
51
|
+
follow_redirects(url, headers) do |current_url, request_headers|
|
|
52
|
+
attempt_buffered(current_url, request_headers)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def post(url : String | URI, body : String, headers : HTTP::Headers? = nil) : Response
|
|
57
|
+
request_headers = HTTP::Headers.new
|
|
58
|
+
copy_headers(headers, request_headers) if headers
|
|
59
|
+
request_headers["Content-Type"] ||= "application/json"
|
|
60
|
+
|
|
61
|
+
follow_redirects(url, request_headers) do |current_url, headers_for_request|
|
|
62
|
+
attempt_buffered(current_url, headers_for_request, "POST", body)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def stream_to(
|
|
67
|
+
url : String | URI,
|
|
68
|
+
destination : IO,
|
|
69
|
+
headers : HTTP::Headers? = nil,
|
|
70
|
+
max_bytes : Int64? = nil,
|
|
71
|
+
&before_body : HTTP::Status, HTTP::Headers, URI ->
|
|
72
|
+
) : Response
|
|
73
|
+
follow_redirects(url, headers) do |current_url, request_headers|
|
|
74
|
+
attempt_stream(current_url, request_headers, destination, max_bytes) do |status, response_headers, response_url|
|
|
75
|
+
before_body.call(status, response_headers, response_url)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stream_to(
|
|
81
|
+
url : String | URI,
|
|
82
|
+
destination : IO,
|
|
83
|
+
headers : HTTP::Headers? = nil,
|
|
84
|
+
max_bytes : Int64? = nil,
|
|
85
|
+
) : Response
|
|
86
|
+
stream_to(url, destination, headers, max_bytes) { |_, _, _| }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private def follow_redirects(
|
|
90
|
+
url : String | URI,
|
|
91
|
+
extra_headers : HTTP::Headers?,
|
|
92
|
+
&request : URI, HTTP::Headers -> Response
|
|
93
|
+
) : Response
|
|
94
|
+
current_url = parse_url(url)
|
|
95
|
+
origin_url = current_url
|
|
96
|
+
headers = initial_headers(extra_headers)
|
|
97
|
+
extra_cookies = parse_cookie_values(extra_headers.try(&.get?("Cookie")))
|
|
98
|
+
redirects = 0
|
|
99
|
+
|
|
100
|
+
loop do
|
|
101
|
+
same_origin = same_origin?(origin_url, current_url)
|
|
102
|
+
apply_cookies(headers, current_url, extra_cookies, same_origin)
|
|
103
|
+
response = request.call(current_url, headers)
|
|
104
|
+
@cookies.add(response.headers, current_url)
|
|
105
|
+
|
|
106
|
+
if location = redirect_location(response)
|
|
107
|
+
if redirects >= @options.max_redirects
|
|
108
|
+
raise TooManyRedirectsError.new(current_url, @options.max_redirects)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
target_url = current_url.resolve(location)
|
|
112
|
+
validate_http_url!(target_url)
|
|
113
|
+
headers = redirected_headers(headers, current_url, target_url)
|
|
114
|
+
current_url = target_url
|
|
115
|
+
redirects += 1
|
|
116
|
+
next
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unless response.status.success?
|
|
120
|
+
raise HTTPError.new(current_url, response.status, response.body)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return response
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private def attempt_buffered(
|
|
128
|
+
url : URI,
|
|
129
|
+
headers : HTTP::Headers,
|
|
130
|
+
method : String = "GET",
|
|
131
|
+
body : String? = nil,
|
|
132
|
+
) : Response
|
|
133
|
+
attempt = 0
|
|
134
|
+
|
|
135
|
+
loop do
|
|
136
|
+
attempt += 1
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
response = perform_buffered(url, headers, method, body)
|
|
140
|
+
rescue ex : NetworkError
|
|
141
|
+
raise ex if attempt > @options.retries
|
|
142
|
+
sleep(retry_wait(attempt))
|
|
143
|
+
next
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return response unless retryable?(response.status) && attempt <= @options.retries
|
|
147
|
+
|
|
148
|
+
sleep(retry_wait(attempt))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private def perform_buffered(
|
|
153
|
+
url : URI,
|
|
154
|
+
headers : HTTP::Headers,
|
|
155
|
+
method : String,
|
|
156
|
+
request_body : String?,
|
|
157
|
+
) : Response
|
|
158
|
+
body = IO::Memory.new
|
|
159
|
+
status = HTTP::Status::OK
|
|
160
|
+
response_headers = HTTP::Headers.new
|
|
161
|
+
|
|
162
|
+
with_client(url) do |client|
|
|
163
|
+
client.exec(build_request(url, headers, method, request_body)) do |response|
|
|
164
|
+
status = response.status
|
|
165
|
+
response_headers = response.headers
|
|
166
|
+
|
|
167
|
+
if status.success?
|
|
168
|
+
read_body(response, url, body, @options.max_response_bytes)
|
|
169
|
+
else
|
|
170
|
+
body << read_error_body(response)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
Response.new(status, body.to_s, response_headers, url)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private def attempt_stream(
|
|
179
|
+
url : URI,
|
|
180
|
+
headers : HTTP::Headers,
|
|
181
|
+
destination : IO,
|
|
182
|
+
max_bytes : Int64?,
|
|
183
|
+
&before_body : HTTP::Status, HTTP::Headers, URI ->
|
|
184
|
+
) : Response
|
|
185
|
+
status = HTTP::Status::OK
|
|
186
|
+
response_headers = HTTP::Headers.new
|
|
187
|
+
|
|
188
|
+
with_client(url) do |client|
|
|
189
|
+
client.exec(build_request(url, headers)) do |response|
|
|
190
|
+
status = response.status
|
|
191
|
+
response_headers = response.headers
|
|
192
|
+
|
|
193
|
+
unless status.success?
|
|
194
|
+
return Response.new(status, read_error_body(response), response_headers, url)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
before_body.call(status, response_headers, url)
|
|
198
|
+
read_body(response, url, destination, max_bytes)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
Response.new(status, "", response_headers, url)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private def read_body(
|
|
206
|
+
response : HTTP::Client::Response,
|
|
207
|
+
url : URI,
|
|
208
|
+
destination : IO,
|
|
209
|
+
max_bytes : Int64?,
|
|
210
|
+
) : Nil
|
|
211
|
+
content_length = response.headers["Content-Length"]?.try(&.to_i64?)
|
|
212
|
+
if max_bytes && content_length && content_length > max_bytes
|
|
213
|
+
raise ResponseTooLargeError.new(url, max_bytes)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
io = response.body_io?
|
|
217
|
+
return unless io
|
|
218
|
+
|
|
219
|
+
buffer = Bytes.new(16 * 1024)
|
|
220
|
+
total = 0_i64
|
|
221
|
+
|
|
222
|
+
loop do
|
|
223
|
+
read = io.read(buffer)
|
|
224
|
+
break if read.zero?
|
|
225
|
+
|
|
226
|
+
total += read
|
|
227
|
+
if max_bytes && total > max_bytes
|
|
228
|
+
raise ResponseTooLargeError.new(url, max_bytes)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
destination.write(buffer[0, read])
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if content_length && identity_encoded?(response) && total < content_length
|
|
235
|
+
raise IncompleteResponseError.new(url, content_length, total)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private def read_error_body(response : HTTP::Client::Response) : String
|
|
240
|
+
io = response.body_io?
|
|
241
|
+
return "" unless io
|
|
242
|
+
|
|
243
|
+
body = IO::Memory.new
|
|
244
|
+
buffer = Bytes.new(4 * 1024)
|
|
245
|
+
remaining = 64 * 1024
|
|
246
|
+
|
|
247
|
+
while remaining > 0
|
|
248
|
+
read = io.read(buffer[0, Math.min(buffer.size, remaining)])
|
|
249
|
+
break if read.zero?
|
|
250
|
+
|
|
251
|
+
body.write(buffer[0, read])
|
|
252
|
+
remaining -= read
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
body.to_s
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private def with_client(url : URI, &)
|
|
259
|
+
client = build_client(url)
|
|
260
|
+
|
|
261
|
+
begin
|
|
262
|
+
yield client
|
|
263
|
+
rescue ex : IO::Error | Socket::Error
|
|
264
|
+
raise NetworkError.new(url, ex)
|
|
265
|
+
ensure
|
|
266
|
+
client.close
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private def build_request(
|
|
271
|
+
url : URI,
|
|
272
|
+
headers : HTTP::Headers,
|
|
273
|
+
method : String = "GET",
|
|
274
|
+
body : String? = nil,
|
|
275
|
+
) : HTTP::Request
|
|
276
|
+
request = HTTP::Request.new(method, request_target(url), headers)
|
|
277
|
+
request.body = body if body
|
|
278
|
+
request
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private def request_target(url : URI) : String
|
|
282
|
+
encode_target(url.request_target)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private def encode_target(value : String) : String
|
|
286
|
+
String.build do |io|
|
|
287
|
+
bytes = value.to_slice
|
|
288
|
+
index = 0
|
|
289
|
+
|
|
290
|
+
while index < bytes.size
|
|
291
|
+
byte = bytes[index]
|
|
292
|
+
|
|
293
|
+
if byte == 0x25_u8 && index + 2 < bytes.size &&
|
|
294
|
+
hex_digit?(bytes[index + 1]) && hex_digit?(bytes[index + 2])
|
|
295
|
+
io.write_byte(byte)
|
|
296
|
+
io.write_byte(bytes[index + 1])
|
|
297
|
+
io.write_byte(bytes[index + 2])
|
|
298
|
+
index += 3
|
|
299
|
+
elsif target_byte?(byte)
|
|
300
|
+
io.write_byte(byte)
|
|
301
|
+
index += 1
|
|
302
|
+
else
|
|
303
|
+
io << '%'
|
|
304
|
+
io.write_byte(HEX.to_unsafe[byte >> 4])
|
|
305
|
+
io.write_byte(HEX.to_unsafe[byte & 0x0f])
|
|
306
|
+
index += 1
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
private def hex_digit?(byte : UInt8) : Bool
|
|
313
|
+
(0x30_u8 <= byte <= 0x39_u8) ||
|
|
314
|
+
(0x41_u8 <= byte <= 0x46_u8) ||
|
|
315
|
+
(0x61_u8 <= byte <= 0x66_u8)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private def target_byte?(byte : UInt8) : Bool
|
|
319
|
+
(0x30_u8 <= byte <= 0x39_u8) ||
|
|
320
|
+
(0x41_u8 <= byte <= 0x5a_u8) ||
|
|
321
|
+
(0x61_u8 <= byte <= 0x7a_u8) ||
|
|
322
|
+
"-._~!$&'()*+,;=:@/?".to_slice.includes?(byte)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private def identity_encoded?(response : HTTP::Client::Response) : Bool
|
|
326
|
+
encoding = response.headers["Content-Encoding"]?
|
|
327
|
+
encoding.nil? || encoding.downcase == "identity"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
private def build_client(url : URI) : HTTP::Client
|
|
331
|
+
client = HTTP::Client.new(
|
|
332
|
+
url.host.not_nil!,
|
|
333
|
+
url.port,
|
|
334
|
+
tls: url.scheme == "https",
|
|
335
|
+
)
|
|
336
|
+
client.connect_timeout = @options.connect_timeout
|
|
337
|
+
client.read_timeout = @options.read_timeout
|
|
338
|
+
client.dns_timeout = @options.connect_timeout
|
|
339
|
+
client
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
private def initial_headers(extra : HTTP::Headers?) : HTTP::Headers
|
|
343
|
+
headers = HTTP::Headers.new
|
|
344
|
+
copy_headers(@options.headers, headers)
|
|
345
|
+
|
|
346
|
+
if extra
|
|
347
|
+
copy_headers(extra, headers)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
headers["User-Agent"] ||= @options.user_agent
|
|
351
|
+
headers["Accept"] ||= "*/*"
|
|
352
|
+
|
|
353
|
+
headers
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
private def apply_cookies(
|
|
357
|
+
headers : HTTP::Headers,
|
|
358
|
+
url : URI,
|
|
359
|
+
extra_cookies : Hash(String, String),
|
|
360
|
+
include_explicit : Bool,
|
|
361
|
+
) : Nil
|
|
362
|
+
headers.delete("Cookie")
|
|
363
|
+
|
|
364
|
+
explicit = {} of String => String
|
|
365
|
+
if include_explicit
|
|
366
|
+
@header_cookies.each do |name, value|
|
|
367
|
+
explicit[name] = value
|
|
368
|
+
end
|
|
369
|
+
extra_cookies.each do |name, value|
|
|
370
|
+
explicit[name] = value
|
|
371
|
+
end
|
|
372
|
+
@options.cookies.each do |name, value|
|
|
373
|
+
explicit[name] = value
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
jar = @cookies.cookies_for(url).reject { |cookie| explicit.has_key?(cookie.name) }
|
|
378
|
+
jar.sort_by! { |cookie| -(cookie.path || "/").size }
|
|
379
|
+
|
|
380
|
+
values = jar.map(&.to_cookie_header)
|
|
381
|
+
explicit.each do |name, value|
|
|
382
|
+
values << "#{name}=#{value}"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
return if values.empty?
|
|
386
|
+
|
|
387
|
+
headers["Cookie"] = values.join("; ")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
private def parse_cookie_values(values : Array(String)?) : Hash(String, String)
|
|
391
|
+
cookies = {} of String => String
|
|
392
|
+
return cookies unless values
|
|
393
|
+
|
|
394
|
+
values.each do |header|
|
|
395
|
+
header.split(';').each do |pair|
|
|
396
|
+
name, separator, value = pair.partition('=')
|
|
397
|
+
next if separator.empty? || name.strip.empty?
|
|
398
|
+
|
|
399
|
+
cookies[name.strip] = value.strip
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
cookies
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
private def copy_headers(source : HTTP::Headers, destination : HTTP::Headers) : Nil
|
|
407
|
+
source.each do |name, values|
|
|
408
|
+
values.each do |value|
|
|
409
|
+
destination.add(name, value)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
private def redirected_headers(
|
|
415
|
+
headers : HTTP::Headers,
|
|
416
|
+
from : URI,
|
|
417
|
+
to : URI,
|
|
418
|
+
) : HTTP::Headers
|
|
419
|
+
return headers if same_origin?(from, to)
|
|
420
|
+
|
|
421
|
+
copy = HTTP::Headers.new
|
|
422
|
+
headers.each do |name, values|
|
|
423
|
+
next if SENSITIVE_HEADERS.includes?(name.downcase)
|
|
424
|
+
|
|
425
|
+
values.each do |value|
|
|
426
|
+
copy.add(name, value)
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
copy
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private def same_origin?(first : URI, second : URI) : Bool
|
|
433
|
+
first.scheme == second.scheme &&
|
|
434
|
+
first.host.try(&.downcase) == second.host.try(&.downcase) &&
|
|
435
|
+
effective_port(first) == effective_port(second)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
private def effective_port(url : URI) : Int32
|
|
439
|
+
url.port || (url.scheme == "https" ? 443 : 80)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
private def redirect_location(response : Response) : String?
|
|
443
|
+
return nil unless REDIRECT_STATUSES.includes?(response.status.code)
|
|
444
|
+
response.headers["Location"]?
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
private def retryable?(status : HTTP::Status) : Bool
|
|
448
|
+
RETRY_STATUSES.includes?(status.code)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
private def retry_wait(attempt : Int32) : Time::Span
|
|
452
|
+
@options.retry_wait(attempt)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
private def parse_url(url : String | URI) : URI
|
|
456
|
+
uri = url.is_a?(URI) ? url : URI.parse(url)
|
|
457
|
+
validate_http_url!(uri)
|
|
458
|
+
uri
|
|
459
|
+
rescue ex : URI::Error
|
|
460
|
+
raise InvalidUrlError.new(url.to_s, ex.message || "malformed URL")
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
private def validate_http_url!(url : URI) : Nil
|
|
464
|
+
unless url.scheme == "http" || url.scheme == "https"
|
|
465
|
+
raise InvalidUrlError.new(url.to_s, "only HTTP and HTTPS are supported")
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
if url.host.nil? || url.host.not_nil!.empty?
|
|
469
|
+
raise InvalidUrlError.new(url.to_s, "missing host")
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
private def validate_options! : Nil
|
|
474
|
+
if @options.retries < 0
|
|
475
|
+
raise ArgumentError.new("retries must not be negative")
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
if @options.retries > 10
|
|
479
|
+
raise ArgumentError.new("retries must not exceed 10")
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
if @options.max_redirects < 0
|
|
483
|
+
raise ArgumentError.new("max_redirects must not be negative")
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
if @options.max_response_bytes <= 0
|
|
487
|
+
raise ArgumentError.new("max_response_bytes must be positive")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
if @options.connect_timeout <= Time::Span.zero
|
|
491
|
+
raise ArgumentError.new("connect_timeout must be positive")
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
if @options.read_timeout <= Time::Span.zero
|
|
495
|
+
raise ArgumentError.new("read_timeout must be positive")
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
if @options.user_agent.empty?
|
|
499
|
+
raise ArgumentError.new("user_agent must not be empty")
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module Rill
|
|
4
|
+
module HttpUrl
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def absolute(value : String?) : URI?
|
|
8
|
+
return nil unless value
|
|
9
|
+
|
|
10
|
+
validate(URI.parse(value))
|
|
11
|
+
rescue URI::Error
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve(base : URI, value : String) : URI?
|
|
16
|
+
validate(base.resolve(value))
|
|
17
|
+
rescue URI::Error
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private def validate(uri : URI) : URI?
|
|
22
|
+
return nil unless uri.scheme == "http" || uri.scheme == "https"
|
|
23
|
+
return nil if uri.host.nil?
|
|
24
|
+
|
|
25
|
+
uri
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Rill
|
|
4
|
+
module JsonValue
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
def string(value : JSON::Any?) : String?
|
|
8
|
+
return nil unless value
|
|
9
|
+
|
|
10
|
+
if text = value.as_s?
|
|
11
|
+
text
|
|
12
|
+
elsif array = value.as_a?
|
|
13
|
+
array.each do |item|
|
|
14
|
+
found = item.as_s?
|
|
15
|
+
return found if found
|
|
16
|
+
end
|
|
17
|
+
nil
|
|
18
|
+
elsif integer = value.as_i64?
|
|
19
|
+
integer.to_s
|
|
20
|
+
elsif float = value.as_f?
|
|
21
|
+
float.to_s
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def integer(value : JSON::Any?) : Int64?
|
|
26
|
+
return nil unless value
|
|
27
|
+
|
|
28
|
+
if text = value.as_s?
|
|
29
|
+
text.to_i64?
|
|
30
|
+
else
|
|
31
|
+
value.as_i64?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def number(value : JSON::Any?) : Float64?
|
|
36
|
+
return nil unless value
|
|
37
|
+
|
|
38
|
+
parsed = if text = value.as_s?
|
|
39
|
+
text.to_f?
|
|
40
|
+
elsif integer = value.as_i64?
|
|
41
|
+
integer.to_f
|
|
42
|
+
elsif float = value.as_f?
|
|
43
|
+
float
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
parsed if parsed && parsed.finite?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def positive_number(value : JSON::Any?) : Float64?
|
|
50
|
+
parsed = number(value)
|
|
51
|
+
parsed if parsed && parsed > 0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dimension(value : JSON::Any?) : Int32?
|
|
55
|
+
parsed = integer(value)
|
|
56
|
+
return nil unless parsed && 0 < parsed <= 100_000
|
|
57
|
+
|
|
58
|
+
parsed.to_i32
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def seconds(value : JSON::Any?) : Float64?
|
|
62
|
+
parsed = number(value)
|
|
63
|
+
parsed if parsed && parsed >= 0
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|