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.
@@ -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
@@ -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
- jar = merge_jar(@cookies, cookies)
66
- hdrs['Cookie'] = jar.to_header unless jar.empty?
67
- adapter = adapter_for(cur_url)
68
- adapter.max_retries = retries.nil? ? @retries : retries
69
- adapter.backoff_factor = @backoff_factor
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,proxies || @proxies, verify.nil? ? @verify : verify,cert || @cert, use_auth)
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,proxies || @proxies, verify.nil? ? @verify : verify,cert || @cert, nil)
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Requests
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.4'
5
5
  end
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'