requests_ruby 1.0.2 → 1.0.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -1
- data/README.md +157 -8
- data/lib/requests/adapters.rb +116 -22
- data/lib/requests/api.rb +10 -5
- data/lib/requests/cookies.rb +92 -19
- data/lib/requests/hpack.rb +241 -0
- data/lib/requests/http2.rb +469 -0
- data/lib/requests/sessions.rb +75 -19
- data/lib/requests/utils.rb +18 -0
- data/lib/requests/version.rb +1 -1
- data/lib/requests.rb +2 -0
- metadata +10 -4
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'zlib'
|
|
7
|
+
require 'timeout'
|
|
8
|
+
require 'time'
|
|
9
|
+
|
|
10
|
+
module Requests
|
|
11
|
+
class Http2Response
|
|
12
|
+
def initialize(header_pairs, body)
|
|
13
|
+
@status = nil
|
|
14
|
+
@fields = Hash.new { |h, k| h[k] = [] }
|
|
15
|
+
@headers = CIHash.new
|
|
16
|
+
header_pairs.each do |k, v|
|
|
17
|
+
if k == ':status'
|
|
18
|
+
@status = v
|
|
19
|
+
else
|
|
20
|
+
@fields[k.downcase] << v
|
|
21
|
+
@headers[k] = v
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
@body = body
|
|
25
|
+
end
|
|
26
|
+
def code
|
|
27
|
+
@status
|
|
28
|
+
end
|
|
29
|
+
def message
|
|
30
|
+
(Requests::STATUS_CODES[@status.to_i] || :unknown).to_s.split('_').map(&:capitalize).join(' ')
|
|
31
|
+
end
|
|
32
|
+
def [](k)
|
|
33
|
+
@headers[k]
|
|
34
|
+
end
|
|
35
|
+
def each_header
|
|
36
|
+
return enum_for(:each_header) unless block_given?
|
|
37
|
+
@headers.each { |k, v| yield k, v }
|
|
38
|
+
end
|
|
39
|
+
def get_fields(name)
|
|
40
|
+
f = @fields[name.to_s.downcase]
|
|
41
|
+
f.empty? ? nil : f
|
|
42
|
+
end
|
|
43
|
+
def body
|
|
44
|
+
@body
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Http2Connection
|
|
49
|
+
TYPE_DATA = 0x0
|
|
50
|
+
TYPE_HEADERS = 0x1
|
|
51
|
+
TYPE_PRIORITY = 0x2
|
|
52
|
+
TYPE_RST_STREAM = 0x3
|
|
53
|
+
TYPE_SETTINGS = 0x4
|
|
54
|
+
TYPE_PUSH_PROMISE = 0x5
|
|
55
|
+
TYPE_PING = 0x6
|
|
56
|
+
TYPE_GOAWAY = 0x7
|
|
57
|
+
TYPE_WINDOW_UPDATE = 0x8
|
|
58
|
+
TYPE_CONTINUATION = 0x9
|
|
59
|
+
FLAG_END_STREAM = 0x1
|
|
60
|
+
FLAG_ACK = 0x1
|
|
61
|
+
FLAG_END_HEADERS = 0x4
|
|
62
|
+
FLAG_PADDED = 0x8
|
|
63
|
+
FLAG_PRIORITY = 0x20
|
|
64
|
+
PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
65
|
+
DEFAULT_WINDOW = 65_535
|
|
66
|
+
|
|
67
|
+
def initialize(socket)
|
|
68
|
+
@socket = socket
|
|
69
|
+
@next_stream_id = 1
|
|
70
|
+
@encoder = HPACK::Encoder.new
|
|
71
|
+
@decoder = HPACK::Decoder.new
|
|
72
|
+
@send_window = DEFAULT_WINDOW
|
|
73
|
+
@conn_recv_window = DEFAULT_WINDOW
|
|
74
|
+
@peer_max_frame_size = 16_384
|
|
75
|
+
@peer_initial_window = DEFAULT_WINDOW
|
|
76
|
+
@streams = {}
|
|
77
|
+
@closed = false
|
|
78
|
+
handshake!
|
|
79
|
+
end
|
|
80
|
+
def alive?
|
|
81
|
+
!@closed && !@socket.closed?
|
|
82
|
+
end
|
|
83
|
+
def close
|
|
84
|
+
write_frame(TYPE_GOAWAY, 0, 0, [0, 0].pack('NN')) if alive?
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
ensure
|
|
88
|
+
@closed = true
|
|
89
|
+
@socket.close unless @socket.closed?
|
|
90
|
+
end
|
|
91
|
+
def request(method, uri, header_list, body, io: nil, progress: nil)
|
|
92
|
+
raise Requests::ConnectionError, 'http/2 connection is closed' unless alive?
|
|
93
|
+
stream_id = @next_stream_id
|
|
94
|
+
@next_stream_id += 2
|
|
95
|
+
@streams[stream_id] = { send_window: @peer_initial_window, recv_window: DEFAULT_WINDOW }
|
|
96
|
+
end_stream = body.nil? || body.to_s.empty?
|
|
97
|
+
send_headers(stream_id, header_list, end_stream)
|
|
98
|
+
send_body(stream_id, body) unless end_stream
|
|
99
|
+
read_response(stream_id, io: io, progress: progress)
|
|
100
|
+
ensure
|
|
101
|
+
@streams.delete(stream_id) if stream_id
|
|
102
|
+
end
|
|
103
|
+
private
|
|
104
|
+
def handshake!
|
|
105
|
+
@socket.write(PREFACE)
|
|
106
|
+
write_frame(TYPE_SETTINGS, 0, 0, '')
|
|
107
|
+
loop do
|
|
108
|
+
type, flags, sid, payload = read_frame
|
|
109
|
+
case type
|
|
110
|
+
when TYPE_SETTINGS
|
|
111
|
+
if flags & FLAG_ACK == 0
|
|
112
|
+
apply_settings(payload)
|
|
113
|
+
write_frame(TYPE_SETTINGS, FLAG_ACK, 0, '')
|
|
114
|
+
end
|
|
115
|
+
break
|
|
116
|
+
when TYPE_WINDOW_UPDATE
|
|
117
|
+
apply_window_update(sid, payload)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
def send_headers(stream_id, header_list, end_stream)
|
|
122
|
+
blob = @encoder.encode(header_list)
|
|
123
|
+
offset = 0
|
|
124
|
+
first = true
|
|
125
|
+
loop do
|
|
126
|
+
chunk = blob.byteslice(offset, @peer_max_frame_size) || ''
|
|
127
|
+
offset += chunk.bytesize
|
|
128
|
+
last = offset >= blob.bytesize
|
|
129
|
+
flags = 0
|
|
130
|
+
flags |= FLAG_END_HEADERS if last
|
|
131
|
+
flags |= FLAG_END_STREAM if last && end_stream
|
|
132
|
+
write_frame(first ? TYPE_HEADERS : TYPE_CONTINUATION, flags, stream_id, chunk)
|
|
133
|
+
first = false
|
|
134
|
+
break if last
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
def send_body(stream_id, body)
|
|
138
|
+
pos = 0
|
|
139
|
+
st = @streams[stream_id]
|
|
140
|
+
body = body.dup.force_encoding(Encoding::BINARY)
|
|
141
|
+
loop do
|
|
142
|
+
avail = [st[:send_window], @send_window, @peer_max_frame_size].min
|
|
143
|
+
if avail <= 0
|
|
144
|
+
type, flags, sid, payload = read_frame
|
|
145
|
+
dispatch_control_frame(type, flags, sid, payload)
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
chunk = body.byteslice(pos, [avail, body.bytesize - pos].min)
|
|
149
|
+
pos += chunk.bytesize
|
|
150
|
+
st[:send_window] -= chunk.bytesize
|
|
151
|
+
@send_window -= chunk.bytesize
|
|
152
|
+
last = pos >= body.bytesize
|
|
153
|
+
write_frame(TYPE_DATA, last ? FLAG_END_STREAM : 0, stream_id, chunk)
|
|
154
|
+
break if last
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
def read_response(stream_id, io: nil, progress: nil)
|
|
158
|
+
header_blob = String.new(encoding: Encoding::BINARY)
|
|
159
|
+
headers = nil
|
|
160
|
+
body = io ? nil : String.new(encoding: Encoding::BINARY)
|
|
161
|
+
decoder = nil
|
|
162
|
+
total = 0
|
|
163
|
+
content_length = nil
|
|
164
|
+
done = false
|
|
165
|
+
until done
|
|
166
|
+
type, flags, sid, payload = read_frame
|
|
167
|
+
case type
|
|
168
|
+
when TYPE_HEADERS, TYPE_CONTINUATION
|
|
169
|
+
next unless sid == stream_id || headers.nil?
|
|
170
|
+
block = type == TYPE_HEADERS ? strip_header_padding(payload, flags) : payload
|
|
171
|
+
header_blob << block
|
|
172
|
+
if flags & FLAG_END_HEADERS != 0
|
|
173
|
+
headers = @decoder.decode(header_blob)
|
|
174
|
+
if io
|
|
175
|
+
hdrs = CIHash.new
|
|
176
|
+
headers.each { |k, v| hdrs[k] = v unless k == ':status' }
|
|
177
|
+
decoder = stream_decoder(hdrs)
|
|
178
|
+
content_length = hdrs['content-length']&.to_i
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
done = true if sid == stream_id && flags & FLAG_END_STREAM != 0
|
|
182
|
+
when TYPE_DATA
|
|
183
|
+
if sid == stream_id
|
|
184
|
+
chunk = strip_data_padding(payload, flags)
|
|
185
|
+
update_recv_window(stream_id, payload.bytesize)
|
|
186
|
+
if io
|
|
187
|
+
piece = decoder == :raw ? chunk : decoder.inflate(chunk)
|
|
188
|
+
io.write(piece)
|
|
189
|
+
total += piece.bytesize
|
|
190
|
+
progress.call(total, content_length) if progress
|
|
191
|
+
else
|
|
192
|
+
body << chunk
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
done = true if sid == stream_id && flags & FLAG_END_STREAM != 0
|
|
196
|
+
else
|
|
197
|
+
dispatch_control_frame(type, flags, sid, payload)
|
|
198
|
+
done = true if type == TYPE_RST_STREAM && sid == stream_id
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
Http2Response.new(headers || [], body)
|
|
202
|
+
end
|
|
203
|
+
def dispatch_control_frame(type, flags, sid, payload)
|
|
204
|
+
case type
|
|
205
|
+
when TYPE_WINDOW_UPDATE
|
|
206
|
+
apply_window_update(sid, payload)
|
|
207
|
+
when TYPE_SETTINGS
|
|
208
|
+
if flags & FLAG_ACK == 0
|
|
209
|
+
apply_settings(payload)
|
|
210
|
+
write_frame(TYPE_SETTINGS, FLAG_ACK, 0, '')
|
|
211
|
+
end
|
|
212
|
+
when TYPE_PING
|
|
213
|
+
write_frame(TYPE_PING, FLAG_ACK, 0, payload) if flags & FLAG_ACK == 0
|
|
214
|
+
when TYPE_GOAWAY
|
|
215
|
+
@closed = true
|
|
216
|
+
when TYPE_RST_STREAM
|
|
217
|
+
raise Requests::ConnectionError, 'stream reset by server'
|
|
218
|
+
when TYPE_PUSH_PROMISE
|
|
219
|
+
psid = payload.byteslice(0, 4).unpack1('N') & 0x7fffffff
|
|
220
|
+
write_frame(TYPE_RST_STREAM, 0, psid, [0x8].pack('N'))
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
def apply_settings(payload)
|
|
224
|
+
i = 0
|
|
225
|
+
while i + 6 <= payload.bytesize
|
|
226
|
+
id = payload.byteslice(i, 2).unpack1('n')
|
|
227
|
+
val = payload.byteslice(i + 2, 4).unpack1('N')
|
|
228
|
+
@peer_initial_window = val if id == 4
|
|
229
|
+
@peer_max_frame_size = val if id == 5
|
|
230
|
+
i += 6
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
def apply_window_update(sid, payload)
|
|
234
|
+
inc = payload.unpack1('N') & 0x7fffffff
|
|
235
|
+
if sid == 0
|
|
236
|
+
@send_window += inc
|
|
237
|
+
else
|
|
238
|
+
(@streams[sid] ||= { send_window: @peer_initial_window, recv_window: DEFAULT_WINDOW })[:send_window] += inc
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
def update_recv_window(stream_id, n)
|
|
242
|
+
st = @streams[stream_id]
|
|
243
|
+
return unless st
|
|
244
|
+
st[:recv_window] -= n
|
|
245
|
+
@conn_recv_window -= n
|
|
246
|
+
if st[:recv_window] < 32_768
|
|
247
|
+
inc = DEFAULT_WINDOW - st[:recv_window]
|
|
248
|
+
write_frame(TYPE_WINDOW_UPDATE, 0, stream_id, [inc].pack('N'))
|
|
249
|
+
st[:recv_window] += inc
|
|
250
|
+
end
|
|
251
|
+
return unless @conn_recv_window < 32_768
|
|
252
|
+
inc = DEFAULT_WINDOW - @conn_recv_window
|
|
253
|
+
write_frame(TYPE_WINDOW_UPDATE, 0, 0, [inc].pack('N'))
|
|
254
|
+
@conn_recv_window += inc
|
|
255
|
+
end
|
|
256
|
+
def stream_decoder(hdrs)
|
|
257
|
+
case hdrs['content-encoding'].to_s.downcase
|
|
258
|
+
when 'gzip', 'x-gzip' then Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
|
|
259
|
+
when 'deflate' then Zlib::Inflate.new
|
|
260
|
+
else :raw
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
def strip_header_padding(payload, flags)
|
|
264
|
+
pos = 0
|
|
265
|
+
padlen = 0
|
|
266
|
+
if flags & FLAG_PADDED != 0
|
|
267
|
+
padlen = payload.getbyte(0)
|
|
268
|
+
pos += 1
|
|
269
|
+
end
|
|
270
|
+
pos += 5 if flags & FLAG_PRIORITY != 0
|
|
271
|
+
payload.byteslice(pos, payload.bytesize - pos - padlen)
|
|
272
|
+
end
|
|
273
|
+
def strip_data_padding(payload, flags)
|
|
274
|
+
return payload if flags & FLAG_PADDED == 0
|
|
275
|
+
padlen = payload.getbyte(0)
|
|
276
|
+
payload.byteslice(1, payload.bytesize - 1 - padlen)
|
|
277
|
+
end
|
|
278
|
+
def read_frame
|
|
279
|
+
hdr = read_exactly(9)
|
|
280
|
+
length = (hdr.getbyte(0) << 16) | (hdr.getbyte(1) << 8) | hdr.getbyte(2)
|
|
281
|
+
type = hdr.getbyte(3)
|
|
282
|
+
flags = hdr.getbyte(4)
|
|
283
|
+
sid = hdr.byteslice(5, 4).unpack1('N') & 0x7fffffff
|
|
284
|
+
payload = length.positive? ? read_exactly(length) : ''
|
|
285
|
+
[type, flags, sid, payload]
|
|
286
|
+
end
|
|
287
|
+
def write_frame(type, flags, stream_id, payload)
|
|
288
|
+
len = payload.bytesize
|
|
289
|
+
hdr = [(len >> 16) & 0xff, (len >> 8) & 0xff, len & 0xff, type, flags].pack('C5') + [stream_id & 0x7fffffff].pack('N')
|
|
290
|
+
@socket.write(hdr + payload)
|
|
291
|
+
end
|
|
292
|
+
def read_exactly(n)
|
|
293
|
+
buf = String.new(encoding: Encoding::BINARY)
|
|
294
|
+
while buf.bytesize < n
|
|
295
|
+
chunk = @socket.read(n - buf.bytesize)
|
|
296
|
+
raise Requests::ConnectionError, 'connection closed by peer' if chunk.nil?
|
|
297
|
+
buf << chunk
|
|
298
|
+
end
|
|
299
|
+
buf
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
class Http2Adapter
|
|
304
|
+
attr_accessor :max_retries, :backoff_factor, :status_forcelist
|
|
305
|
+
def initialize(max_retries: 0, backoff_factor: 0, status_forcelist: [])
|
|
306
|
+
@max_retries = max_retries
|
|
307
|
+
@backoff_factor = backoff_factor
|
|
308
|
+
@status_forcelist = status_forcelist
|
|
309
|
+
@pool = {}
|
|
310
|
+
@alpn = {}
|
|
311
|
+
@fallback = HTTPAdapter.new(max_retries: max_retries, backoff_factor: backoff_factor, status_forcelist: status_forcelist)
|
|
312
|
+
end
|
|
313
|
+
def send_once(meth, url, hdrs, body, timeout, proxies, verify, cert, auth)
|
|
314
|
+
u = URI.parse(url)
|
|
315
|
+
sync_fallback!
|
|
316
|
+
return @fallback.send_once(meth, url, hdrs, body, timeout, proxies, verify, cert, auth) unless usable?(u, proxies)
|
|
317
|
+
conn = conn_for(u, verify, cert, timeout)
|
|
318
|
+
return @fallback.send_once(meth, url, hdrs, body, timeout, proxies, verify, cert, auth) if conn == :http1
|
|
319
|
+
attempt = 0
|
|
320
|
+
loop do
|
|
321
|
+
begin
|
|
322
|
+
auth.call(hdrs) if auth.respond_to?(:call) && !auth.is_a?(Requests::DigestAuth)
|
|
323
|
+
resp = perform(conn, meth, u, hdrs, body, timeout)
|
|
324
|
+
if @status_forcelist.include?(resp.code.to_i) && attempt < @max_retries
|
|
325
|
+
attempt += 1
|
|
326
|
+
sleep(retry_delay(resp, attempt))
|
|
327
|
+
conn = conn_for(u, verify, cert, timeout)
|
|
328
|
+
next
|
|
329
|
+
end
|
|
330
|
+
return resp
|
|
331
|
+
rescue Requests::ConnectionError, Requests::ConnectTimeout => e
|
|
332
|
+
drop_conn(u)
|
|
333
|
+
attempt += 1
|
|
334
|
+
raise e unless attempt <= @max_retries && idempotent?(meth)
|
|
335
|
+
sleep(@backoff_factor * attempt) if @backoff_factor.to_f > 0
|
|
336
|
+
conn = conn_for(u, verify, cert, timeout)
|
|
337
|
+
return @fallback.send_once(meth, url, hdrs, body, timeout, proxies, verify, cert, auth) if conn == :http1
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
def stream_to(meth, url, hdrs, body, timeout, proxies, verify, cert, auth, io, chunk_size: 65_536, progress: nil)
|
|
342
|
+
u = URI.parse(url)
|
|
343
|
+
sync_fallback!
|
|
344
|
+
unless usable?(u, proxies)
|
|
345
|
+
return @fallback.stream_to(meth, url, hdrs, body, timeout, proxies, verify, cert, auth, io, chunk_size: chunk_size, progress: progress)
|
|
346
|
+
end
|
|
347
|
+
conn = conn_for(u, verify, cert, timeout)
|
|
348
|
+
if conn == :http1
|
|
349
|
+
return @fallback.stream_to(meth, url, hdrs, body, timeout, proxies, verify, cert, auth, io, chunk_size: chunk_size, progress: progress)
|
|
350
|
+
end
|
|
351
|
+
auth.call(hdrs) if auth.respond_to?(:call) && !auth.is_a?(Requests::DigestAuth)
|
|
352
|
+
begin
|
|
353
|
+
header_list = build_headers(meth, u, hdrs)
|
|
354
|
+
_open_t, read_t = split_timeout(timeout)
|
|
355
|
+
with_timeout(read_t) { conn.request(meth, u, header_list, body, io: io, progress: progress) }
|
|
356
|
+
rescue Requests::ConnectionError, Requests::ConnectTimeout => e
|
|
357
|
+
drop_conn(u)
|
|
358
|
+
raise e
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
def decode_body(net_resp)
|
|
362
|
+
@fallback.decode_body(net_resp)
|
|
363
|
+
end
|
|
364
|
+
def close_pool
|
|
365
|
+
@pool.each_value { |c| c.close }
|
|
366
|
+
@pool.clear
|
|
367
|
+
@fallback.close_pool
|
|
368
|
+
end
|
|
369
|
+
private
|
|
370
|
+
def perform(conn, meth, u, hdrs, body, timeout)
|
|
371
|
+
header_list = build_headers(meth, u, hdrs)
|
|
372
|
+
_open_t, read_t = split_timeout(timeout)
|
|
373
|
+
with_timeout(read_t) { conn.request(meth, u, header_list, body) }
|
|
374
|
+
end
|
|
375
|
+
def with_timeout(read_t, &blk)
|
|
376
|
+
return blk.call unless read_t
|
|
377
|
+
::Timeout.timeout(read_t, Requests::ReadTimeout, &blk)
|
|
378
|
+
end
|
|
379
|
+
def sync_fallback!
|
|
380
|
+
@fallback.max_retries = @max_retries
|
|
381
|
+
@fallback.backoff_factor = @backoff_factor
|
|
382
|
+
@fallback.status_forcelist = @status_forcelist
|
|
383
|
+
end
|
|
384
|
+
def idempotent?(meth)
|
|
385
|
+
%w[GET HEAD OPTIONS PUT DELETE].include?(meth.to_s.upcase)
|
|
386
|
+
end
|
|
387
|
+
def retry_delay(resp, attempt)
|
|
388
|
+
ra = resp['retry-after']
|
|
389
|
+
if ra
|
|
390
|
+
return ra.to_i if ra =~ /\A\d+\z/
|
|
391
|
+
begin
|
|
392
|
+
d = Time.httpdate(ra) - Time.now
|
|
393
|
+
return d.positive? ? d : 0
|
|
394
|
+
rescue ArgumentError
|
|
395
|
+
nil
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
@backoff_factor.to_f > 0 ? @backoff_factor * attempt : 0
|
|
399
|
+
end
|
|
400
|
+
def usable?(u, proxies)
|
|
401
|
+
return false unless u.scheme == 'https'
|
|
402
|
+
return false if proxies && !proxies.empty? && (proxies[u.scheme] || proxies[u.scheme.to_sym])
|
|
403
|
+
true
|
|
404
|
+
end
|
|
405
|
+
def build_headers(meth, u, hdrs)
|
|
406
|
+
port = u.port && u.port != 443 ? ":#{u.port}" : ''
|
|
407
|
+
list = [[':method', meth.to_s.upcase], [':scheme', 'https'], [':path', u.request_uri], [':authority', "#{u.host}#{port}"]]
|
|
408
|
+
hdrs.each { |k, v| list << [k.to_s, v.to_s] unless k.to_s.downcase == 'host' }
|
|
409
|
+
list
|
|
410
|
+
end
|
|
411
|
+
def key_for(u)
|
|
412
|
+
"#{u.host}:#{u.port}"
|
|
413
|
+
end
|
|
414
|
+
def conn_for(u, verify, cert, timeout)
|
|
415
|
+
k = key_for(u)
|
|
416
|
+
cached = @pool[k]
|
|
417
|
+
return cached if cached && cached.alive?
|
|
418
|
+
@pool.delete(k)
|
|
419
|
+
return :http1 if @alpn[k] == 'http1'
|
|
420
|
+
sock = open_tls(u, verify, cert, timeout)
|
|
421
|
+
if sock.alpn_protocol == 'h2'
|
|
422
|
+
@alpn[k] = 'h2'
|
|
423
|
+
@pool[k] = Http2Connection.new(sock)
|
|
424
|
+
else
|
|
425
|
+
@alpn[k] = 'http1'
|
|
426
|
+
sock.close rescue nil
|
|
427
|
+
:http1
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
def drop_conn(u)
|
|
431
|
+
c = @pool.delete(key_for(u))
|
|
432
|
+
c.close if c
|
|
433
|
+
end
|
|
434
|
+
def open_tls(u, verify, cert, timeout)
|
|
435
|
+
open_t, _read_t = split_timeout(timeout)
|
|
436
|
+
tcp = open_t ? Socket.tcp(u.host, u.port, connect_timeout: open_t) : Socket.tcp(u.host, u.port)
|
|
437
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
438
|
+
ctx.alpn_protocols = ['h2', 'http/1.1']
|
|
439
|
+
if verify == false
|
|
440
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
441
|
+
else
|
|
442
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
443
|
+
ctx.ca_file = verify.is_a?(String) ? verify : Requests::CA_FILE
|
|
444
|
+
end
|
|
445
|
+
if cert
|
|
446
|
+
ctx.cert = OpenSSL::X509::Certificate.new(File.read(cert[0]))
|
|
447
|
+
ctx.key = OpenSSL::PKey::RSA.new(File.read(cert[1]))
|
|
448
|
+
end
|
|
449
|
+
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
|
450
|
+
ssl.hostname = u.host
|
|
451
|
+
ssl.sync_close = true
|
|
452
|
+
ssl.connect
|
|
453
|
+
ssl
|
|
454
|
+
rescue Errno::ETIMEDOUT, IO::TimeoutError
|
|
455
|
+
raise Requests::ConnectTimeout, "connect timeout: #{u}"
|
|
456
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
457
|
+
raise Requests::SSLError, e.message
|
|
458
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET => e
|
|
459
|
+
raise Requests::ConnectionError, e.message
|
|
460
|
+
end
|
|
461
|
+
def split_timeout(t)
|
|
462
|
+
case t
|
|
463
|
+
when nil then [nil, nil]
|
|
464
|
+
when Array then [t[0], t[1]]
|
|
465
|
+
else [t, t]
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
data/lib/requests/sessions.rb
CHANGED
|
@@ -6,8 +6,8 @@ require 'time'
|
|
|
6
6
|
module Requests
|
|
7
7
|
class Session
|
|
8
8
|
attr_accessor :headers, :cookies, :auth, :params, :proxies, :verify,
|
|
9
|
-
:cert, :max_redirects, :timeout, :retries, :backoff_factor
|
|
10
|
-
def initialize
|
|
9
|
+
:cert, :max_redirects, :timeout, :retries, :backoff_factor, :status_forcelist, :trust_env
|
|
10
|
+
def initialize(http2: false)
|
|
11
11
|
@headers = Requests::Utils.default_headers
|
|
12
12
|
@cookies = Jar.new
|
|
13
13
|
@auth = nil
|
|
@@ -19,8 +19,12 @@ module Requests
|
|
|
19
19
|
@timeout = nil
|
|
20
20
|
@retries = 0
|
|
21
21
|
@backoff_factor = 0
|
|
22
|
+
@status_forcelist = []
|
|
23
|
+
@trust_env = true
|
|
22
24
|
@hooks = { response: [] }
|
|
23
|
-
@adapters = { 'https://' => HTTPAdapter.new, 'http://' => HTTPAdapter.new }
|
|
25
|
+
@adapters = { 'https://' => http2 ? Http2Adapter.new : HTTPAdapter.new, 'http://' => HTTPAdapter.new }
|
|
26
|
+
@default_adapters = @adapters.values.dup
|
|
27
|
+
@http2_adapter = http2 ? @adapters['https://'] : nil
|
|
24
28
|
end
|
|
25
29
|
def hooks
|
|
26
30
|
@hooks
|
|
@@ -29,6 +33,12 @@ module Requests
|
|
|
29
33
|
@adapters[prefix] = adapter
|
|
30
34
|
self
|
|
31
35
|
end
|
|
36
|
+
def http2!
|
|
37
|
+
@http2_adapter ||= Http2Adapter.new
|
|
38
|
+
@adapters['https://'] = @http2_adapter
|
|
39
|
+
@default_adapters = @adapters.values.dup
|
|
40
|
+
self
|
|
41
|
+
end
|
|
32
42
|
def get(url, **kw); request('GET', url, **kw); end
|
|
33
43
|
def post(url, **kw); request('POST', url, **kw); end
|
|
34
44
|
def put(url, **kw); request('PUT', url, **kw); end
|
|
@@ -41,12 +51,44 @@ module Requests
|
|
|
41
51
|
end
|
|
42
52
|
def get!(url, **kw); get(url, **kw).raise_for_status; end
|
|
43
53
|
def post!(url, **kw); post(url, **kw).raise_for_status; end
|
|
54
|
+
def put!(url, **kw); put(url, **kw).raise_for_status; end
|
|
55
|
+
def patch!(url, **kw); patch(url, **kw).raise_for_status; end
|
|
56
|
+
def delete!(url, **kw); delete(url, **kw).raise_for_status; end
|
|
57
|
+
def head!(url, **kw); head(url, **kw).raise_for_status; end
|
|
58
|
+
def options!(url, **kw); options(url, **kw).raise_for_status; end
|
|
44
59
|
def close
|
|
60
|
+
@adapters.values.uniq.each { |a| a.close_pool if a.respond_to?(:close_pool) }
|
|
45
61
|
true
|
|
46
62
|
end
|
|
63
|
+
def download(url, to:, chunk_size: 65536, resume: false, progress: nil, headers: nil, params: nil,
|
|
64
|
+
timeout: nil, proxies: nil, verify: nil, cert: nil, auth: nil, http2: nil)
|
|
65
|
+
validate_url!(url)
|
|
66
|
+
full_url = build_url(url, merge_hash(@params, params))
|
|
67
|
+
hdrs = CIHash.new(@headers.to_h)
|
|
68
|
+
hdrs.merge!(headers)
|
|
69
|
+
mode = 'wb'
|
|
70
|
+
if resume && File.exist?(to) && File.size(to) > 0
|
|
71
|
+
hdrs['Range'] = "bytes=#{File.size(to)}-"
|
|
72
|
+
mode = 'ab'
|
|
73
|
+
end
|
|
74
|
+
adapter = pick_adapter(full_url, http2)
|
|
75
|
+
adapter.max_retries = @retries
|
|
76
|
+
adapter.backoff_factor = @backoff_factor
|
|
77
|
+
use_auth = auth || @auth
|
|
78
|
+
use_auth.call(hdrs) if use_auth.respond_to?(:call) && !use_auth.is_a?(DigestAuth)
|
|
79
|
+
File.open(to, mode) do |f|
|
|
80
|
+
net_resp = adapter.stream_to('GET', full_url, hdrs, nil, timeout || @timeout, effective_proxies(full_url, proxies),
|
|
81
|
+
verify.nil? ? @verify : verify, cert || @cert, nil, f,
|
|
82
|
+
chunk_size: chunk_size, progress: progress)
|
|
83
|
+
unless [200, 206].include?(net_resp.code.to_i)
|
|
84
|
+
raise Requests::HTTPError, "download failed: #{net_resp.code} for #{full_url}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
to
|
|
88
|
+
end
|
|
47
89
|
def request(method, url, params: nil, data: nil, json: nil, headers: nil, cookies: nil, files: nil,
|
|
48
90
|
auth: nil, timeout: nil, allow_redirects: true, proxies: nil, verify: nil, stream: false,
|
|
49
|
-
cert: nil, hooks: nil, retries: nil)
|
|
91
|
+
cert: nil, hooks: nil, retries: nil, http2: nil)
|
|
50
92
|
validate_url!(url)
|
|
51
93
|
use_auth = auth || @auth
|
|
52
94
|
merged_params = merge_hash(@params, params)
|
|
@@ -62,23 +104,27 @@ module Requests
|
|
|
62
104
|
hdrs.merge!(headers)
|
|
63
105
|
hdrs['Content-Type'] = content_type if content_type && !hdrs.key?('content-type')
|
|
64
106
|
hdrs.delete('Authorization') if redirs > 0 && URI.parse(cur_url).host != original_host
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
adapter
|
|
69
|
-
adapter.
|
|
107
|
+
ck = @cookies.to_header(cur_url)
|
|
108
|
+
ck = [ck, cookies.map { |k, v| "#{k}=#{v}" }.join('; ')].reject { |x| x.nil? || x.empty? }.join('; ') if cookies
|
|
109
|
+
hdrs['Cookie'] = ck unless ck.empty?
|
|
110
|
+
adapter = pick_adapter(cur_url, http2)
|
|
111
|
+
adapter.max_retries = retries unless retries.nil?
|
|
112
|
+
adapter.max_retries = @retries if @retries.to_i > 0 && retries.nil? && default_adapter?(adapter)
|
|
113
|
+
adapter.backoff_factor = @backoff_factor if @backoff_factor.to_f > 0 && default_adapter?(adapter)
|
|
114
|
+
adapter.status_forcelist = @status_forcelist if !@status_forcelist.empty? && adapter.respond_to?(:status_forcelist=) && default_adapter?(adapter)
|
|
115
|
+
eff_proxies = effective_proxies(cur_url, proxies)
|
|
70
116
|
t0 = Time.now
|
|
71
|
-
net_resp = adapter.send_once(cur_method, cur_url, hdrs, body, timeout || @timeout,
|
|
117
|
+
net_resp = adapter.send_once(cur_method, cur_url, hdrs, body, timeout || @timeout, eff_proxies, verify.nil? ? @verify : verify,cert || @cert, use_auth)
|
|
72
118
|
elapsed = Time.now - t0
|
|
73
119
|
resp = to_response(adapter, net_resp, cur_url, elapsed, cur_method, hdrs, body)
|
|
74
|
-
@cookies.update(net_resp)
|
|
120
|
+
@cookies.update(net_resp, cur_url)
|
|
75
121
|
resp.cookies = @cookies
|
|
76
122
|
if use_auth.is_a?(DigestAuth) && resp.status_code == 401 && redirs.zero? && hist.empty?
|
|
77
123
|
use_auth.call(hdrs, meth: cur_method, url: cur_url, prev_resp: resp)
|
|
78
124
|
t1 = Time.now
|
|
79
|
-
net_resp = adapter.send_once(cur_method, cur_url, hdrs, body, timeout || @timeout,
|
|
125
|
+
net_resp = adapter.send_once(cur_method, cur_url, hdrs, body, timeout || @timeout, eff_proxies, verify.nil? ? @verify : verify,cert || @cert, nil)
|
|
80
126
|
resp = to_response(adapter, net_resp, cur_url, Time.now - t1, cur_method, hdrs, body)
|
|
81
|
-
@cookies.update(net_resp)
|
|
127
|
+
@cookies.update(net_resp, cur_url)
|
|
82
128
|
resp.cookies = @cookies
|
|
83
129
|
end
|
|
84
130
|
hist << resp
|
|
@@ -107,21 +153,31 @@ module Requests
|
|
|
107
153
|
raise Requests::MissingSchema, "invalid url, no scheme: #{url}" unless s.include?('://')
|
|
108
154
|
raise Requests::InvalidSchema, "unsupported scheme: #{url}" unless s =~ %r{\Ahttps?://}
|
|
109
155
|
end
|
|
156
|
+
def default_adapter?(a)
|
|
157
|
+
@default_adapters.include?(a)
|
|
158
|
+
end
|
|
110
159
|
def adapter_for(url)
|
|
111
160
|
match = @adapters.keys.select { |prefix| url.start_with?(prefix) }.max_by(&:length)
|
|
112
161
|
match ? @adapters[match] : @adapters['https://']
|
|
113
162
|
end
|
|
163
|
+
def pick_adapter(url, http2)
|
|
164
|
+
return adapter_for(url) unless http2 == true
|
|
165
|
+
unless @http2_adapter
|
|
166
|
+
@http2_adapter = Http2Adapter.new
|
|
167
|
+
@default_adapters << @http2_adapter
|
|
168
|
+
end
|
|
169
|
+
@http2_adapter
|
|
170
|
+
end
|
|
171
|
+
def effective_proxies(url, proxies)
|
|
172
|
+
p = proxies || @proxies
|
|
173
|
+
return p if p && !p.empty?
|
|
174
|
+
@trust_env ? Requests::Utils.env_proxies(url) : (p || {})
|
|
175
|
+
end
|
|
114
176
|
def merge_hash(a, b)
|
|
115
177
|
return b unless a
|
|
116
178
|
return a unless b
|
|
117
179
|
a.merge(b)
|
|
118
180
|
end
|
|
119
|
-
def merge_jar(session_jar, extra)
|
|
120
|
-
j = Jar.new
|
|
121
|
-
session_jar.each { |k, v| j[k] = v }
|
|
122
|
-
extra&.each { |k, v| j[k] = v }
|
|
123
|
-
j
|
|
124
|
-
end
|
|
125
181
|
def build_url(base, params)
|
|
126
182
|
return base if params.nil? || (params.respond_to?(:empty?) && params.empty?)
|
|
127
183
|
qs = build_qs(params)
|
data/lib/requests/utils.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
3
5
|
module Requests
|
|
4
6
|
module Utils
|
|
5
7
|
module_function
|
|
@@ -51,5 +53,21 @@ module Requests
|
|
|
51
53
|
def looks_like_html?(str)
|
|
52
54
|
!!(str.to_s.lstrip =~ /\A(<!DOCTYPE html|<html)/i)
|
|
53
55
|
end
|
|
56
|
+
def env_proxies(url)
|
|
57
|
+
host = URI.parse(url).host.to_s
|
|
58
|
+
no_proxy = ENV['NO_PROXY'] || ENV['no_proxy']
|
|
59
|
+
if no_proxy
|
|
60
|
+
list = no_proxy.split(',').map { |s| s.strip.downcase }.reject(&:empty?)
|
|
61
|
+
return {} if list.include?('*') || list.any? { |p| host.downcase == p || host.downcase.end_with?(".#{p.sub(/\A\./, '')}") }
|
|
62
|
+
end
|
|
63
|
+
out = {}
|
|
64
|
+
http = ENV['HTTP_PROXY'] || ENV['http_proxy']
|
|
65
|
+
https = ENV['HTTPS_PROXY'] || ENV['https_proxy']
|
|
66
|
+
out['http'] = http if http && !http.empty?
|
|
67
|
+
out['https'] = https if https && !https.empty?
|
|
68
|
+
out
|
|
69
|
+
rescue URI::InvalidURIError
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
54
72
|
end
|
|
55
73
|
end
|
data/lib/requests/version.rb
CHANGED
data/lib/requests.rb
CHANGED
|
@@ -15,5 +15,7 @@ require_relative 'requests/auth'
|
|
|
15
15
|
require_relative 'requests/utils'
|
|
16
16
|
require_relative 'requests/models'
|
|
17
17
|
require_relative 'requests/adapters'
|
|
18
|
+
require_relative 'requests/hpack'
|
|
19
|
+
require_relative 'requests/http2'
|
|
18
20
|
require_relative 'requests/sessions'
|
|
19
21
|
require_relative 'requests/api'
|