excon 0.99.0 → 1.2.5

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.
data/lib/excon/socket.rb CHANGED
@@ -25,6 +25,13 @@ module Excon
25
25
  else # Ruby <= 2.0
26
26
  [Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable]
27
27
  end
28
+ # Maps a socket operation to a timeout property.
29
+ OPERATION_TO_TIMEOUT = {
30
+ :connect_read => :connect_timeout,
31
+ :connect_write => :connect_timeout,
32
+ :read => :read_timeout,
33
+ :write => :write_timeout
34
+ }.freeze
28
35
 
29
36
  def params
30
37
  Excon.display_warning('Excon::Socket#params is deprecated use Excon::Socket#data instead.')
@@ -46,14 +53,16 @@ module Excon
46
53
  @nonblock = data[:nonblock]
47
54
  @port ||= @data[:port] || 80
48
55
  @read_buffer = String.new
56
+ @read_offset = 0
49
57
  @eof = false
50
58
  @backend_eof = false
59
+
51
60
  connect
52
61
  end
53
62
 
54
63
  def read(max_length = nil)
55
64
  if @eof
56
- return max_length ? nil : ''
65
+ max_length ? nil : ''
57
66
  elsif @nonblock
58
67
  read_nonblock(max_length)
59
68
  else
@@ -62,22 +71,24 @@ module Excon
62
71
  end
63
72
 
64
73
  def readline
65
- if @nonblock && RUBY_VERSION.to_f > 1.8_7
74
+ if @nonblock
66
75
  result = String.new
67
- block = @read_buffer
68
- @read_buffer = String.new
76
+ block = consume_read_buffer
69
77
 
70
78
  loop do
71
79
  idx = block.index("\n")
80
+
72
81
  if idx.nil?
73
82
  result << block
74
83
  else
75
- result << block.slice!(0, idx+1)
76
- add_to_read_buffer(block)
84
+ result << block[0..idx]
85
+ rewind_read_buffer(block, idx)
77
86
  break
78
87
  end
88
+
79
89
  block = read_nonblock(@data[:chunk_size]) || raise(EOFError)
80
90
  end
91
+
81
92
  result
82
93
  else # nonblock/legacy
83
94
  begin
@@ -121,7 +132,17 @@ module Excon
121
132
  family = @data[:proxy][:family]
122
133
  end
123
134
 
124
- Resolv.each_address(hostname) do |ip|
135
+ resolver = @data[:resolv_resolver] || Resolv::DefaultResolver
136
+
137
+ # Deprecated
138
+ if @data[:dns_timeouts]
139
+ Excon.display_warning('dns_timeouts is deprecated, use resolv_resolver instead.')
140
+ dns_resolver = Resolv::DNS.new
141
+ dns_resolver.timeouts = @data[:dns_timeouts]
142
+ resolver = Resolv.new([Resolv::Hosts.new, dns_resolver])
143
+ end
144
+
145
+ resolver.each_address(hostname) do |ip|
125
146
  # already succeeded on previous addrinfo
126
147
  if @socket
127
148
  break
@@ -160,7 +181,7 @@ module Excon
160
181
  socket.close rescue nil
161
182
  end
162
183
  rescue SystemCallError => exception
163
- socket.close rescue nil if socket
184
+ socket&.close rescue nil
164
185
  end
165
186
  end
166
187
 
@@ -187,20 +208,48 @@ module Excon
187
208
  end
188
209
  end
189
210
 
190
- def add_to_read_buffer(str)
191
- @read_buffer << str
211
+ # Consume any bytes remaining in the read buffer before making a system call.
212
+ def consume_read_buffer
213
+ block = @read_buffer[@read_offset..-1]
214
+
215
+ @read_offset = @read_buffer.length
216
+
217
+ block
218
+ end
219
+
220
+ # Rewind the read buffer to just after the given index.
221
+ # The offset is moved back to the start of the current chunk and then forward until just after the index.
222
+ def rewind_read_buffer(chunk, idx)
223
+ @read_offset = @read_offset - chunk.length + (idx + 1)
192
224
  @eof = false
193
225
  end
194
226
 
195
227
  def read_nonblock(max_length)
196
228
  begin
229
+ if @read_offset != 0 && @read_offset >= @read_buffer.length
230
+ # Clear the buffer so we can test for emptiness below
231
+ @read_buffer.clear
232
+ # Reset the offset so it matches the length of the buffer when empty.
233
+ @read_offset = 0
234
+ end
235
+
197
236
  if max_length
198
- until @backend_eof || @read_buffer.length >= max_length
199
- @read_buffer << @socket.read_nonblock(max_length - @read_buffer.length)
237
+ until @backend_eof || readable_bytes >= max_length
238
+ if @read_buffer.empty?
239
+ # Avoid allocating a new buffer string when the read buffer is empty
240
+ @read_buffer = @socket.read_nonblock(max_length, @read_buffer)
241
+ else
242
+ @read_buffer << @socket.read_nonblock(max_length - readable_bytes)
243
+ end
200
244
  end
201
245
  else
202
- while !@backend_eof
203
- @read_buffer << @socket.read_nonblock(@data[:chunk_size])
246
+ until @backend_eof
247
+ if @read_buffer.empty?
248
+ # Avoid allocating a new buffer string when the read buffer is empty
249
+ @read_buffer = @socket.read_nonblock(@data[:chunk_size], @read_buffer)
250
+ else
251
+ @read_buffer << @socket.read_nonblock(@data[:chunk_size])
252
+ end
204
253
  end
205
254
  end
206
255
  rescue OpenSSL::SSL::SSLError => error
@@ -220,18 +269,32 @@ module Excon
220
269
  @backend_eof = true
221
270
  end
222
271
 
223
- ret = if max_length
272
+ if max_length
224
273
  if @read_buffer.empty?
225
- nil # EOF met at beginning
274
+ # EOF met at beginning
275
+ @eof = @backend_eof
276
+ nil
226
277
  else
227
- @read_buffer.slice!(0, max_length)
278
+ start = @read_offset
279
+
280
+ # Ensure that we can seek backwards when reading until a terminator string.
281
+ # The read offset must never point past the end of the read buffer.
282
+ @read_offset += max_length > readable_bytes ? readable_bytes : max_length
283
+ @read_buffer[start...@read_offset]
228
284
  end
229
285
  else
230
286
  # read until EOFError, so return everything
231
- @read_buffer.slice!(0, @read_buffer.length)
287
+ start = @read_offset
288
+
289
+ @read_offset = @read_buffer.length
290
+ @eof = @backend_eof
291
+
292
+ @read_buffer[start..-1]
232
293
  end
233
- @eof = @backend_eof && @read_buffer.empty?
234
- ret
294
+ end
295
+
296
+ def readable_bytes
297
+ @read_buffer.length - @read_offset
235
298
  end
236
299
 
237
300
  def read_block(max_length)
@@ -294,17 +357,33 @@ module Excon
294
357
  end
295
358
 
296
359
  def select_with_timeout(socket, type)
360
+ timeout_kind = type
361
+ timeout = @data[OPERATION_TO_TIMEOUT[type]]
362
+
363
+ # Check whether the request has a timeout configured.
364
+ if @data.include?(:deadline)
365
+ request_timeout = request_time_remaining
366
+
367
+ # If the time remaining until the request times out is less than the timeout for the type of select,
368
+ # use the time remaining as the timeout instead.
369
+ if request_timeout < timeout
370
+ timeout_kind = :request
371
+ timeout = request_timeout
372
+ end
373
+ end
374
+
297
375
  select = case type
298
376
  when :connect_read
299
- IO.select([socket], nil, nil, @data[:connect_timeout])
377
+ IO.select([socket], nil, nil, timeout)
300
378
  when :connect_write
301
- IO.select(nil, [socket], nil, @data[:connect_timeout])
379
+ IO.select(nil, [socket], nil, timeout)
302
380
  when :read
303
- IO.select([socket], nil, nil, @data[:read_timeout])
381
+ IO.select([socket], nil, nil, timeout)
304
382
  when :write
305
- IO.select(nil, [socket], nil, @data[:write_timeout])
383
+ IO.select(nil, [socket], nil, timeout)
306
384
  end
307
- select || raise(Excon::Errors::Timeout.new("#{type} timeout reached"))
385
+
386
+ select || raise(Excon::Errors::Timeout.new("#{timeout_kind} timeout reached"))
308
387
  end
309
388
 
310
389
  def unpacked_sockaddr
@@ -314,5 +393,16 @@ module Excon
314
393
  raise
315
394
  end
316
395
  end
396
+
397
+ # Returns the remaining time in seconds until we reach the deadline for the request timeout.
398
+ # Raises an exception if we have exceeded the request timeout's deadline.
399
+ def request_time_remaining
400
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
401
+ deadline = @data[:deadline]
402
+
403
+ raise(Excon::Errors::Timeout.new('request timeout reached')) if now >= deadline
404
+
405
+ deadline - now
406
+ end
317
407
  end
318
408
  end
@@ -26,6 +26,7 @@ module Excon
26
26
  if defined?(OpenSSL::SSL::OP_NO_COMPRESSION)
27
27
  ssl_context_options |= OpenSSL::SSL::OP_NO_COMPRESSION
28
28
  end
29
+ ssl_context_options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF if @data[:ignore_unexpected_eof]
29
30
  ssl_context.options = ssl_context_options
30
31
 
31
32
  ssl_context.ciphers = @data[:ciphers]
@@ -63,7 +64,7 @@ module Excon
63
64
  unless ca_file || ca_path || cert_store
64
65
  # workaround issue #257 (JRUBY-6970)
65
66
  ca_file = DEFAULT_CA_FILE
66
- ca_file = ca_file.gsub(/^jar:/, '') if ca_file =~ /^jar:file:\//
67
+ ca_file = ca_file.gsub(/^jar:/, '') if ca_file.match?(/^jar:file:\//)
67
68
 
68
69
  begin
69
70
  ssl_context.cert_store.add_file(ca_file)
@@ -108,7 +109,7 @@ module Excon
108
109
  end
109
110
 
110
111
  if @data[:proxy]
111
- request = "CONNECT #{@data[:host]}#{port_string(@data.merge(:omit_default_port => false))}#{Excon::HTTP_1_1}" +
112
+ request = "CONNECT #{@data[:host]}#{port_string(@data)}#{Excon::HTTP_1_1}" \
112
113
  "Host: #{@data[:host]}#{port_string(@data)}#{Excon::CR_NL}"
113
114
 
114
115
  if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
@@ -7,7 +7,7 @@ module Excon
7
7
  open_process(RbConfig.ruby, '-S', 'puma', '-b', bind_uri.to_s, app_str)
8
8
  process_stderr = ""
9
9
  line = ''
10
- until line =~ /Use Ctrl-C to stop/
10
+ until line.include?('Use Ctrl-C to stop')
11
11
  line = read.gets
12
12
  raise process_stderr if line.nil?
13
13
  process_stderr << line
@@ -12,19 +12,19 @@ module Excon
12
12
  host = bind_uri.host.gsub(/[\[\]]/, '')
13
13
  bind_str = "#{host}:#{bind_uri.port}"
14
14
  end
15
- args = [
15
+ args = [
16
16
  RbConfig.ruby,
17
17
  '-S',
18
- 'unicorn',
19
- '--no-default-middleware',
18
+ 'unicorn',
19
+ '--no-default-middleware',
20
20
  '-l',
21
- bind_str,
21
+ bind_str,
22
22
  app_str
23
23
  ]
24
24
  open_process(*args)
25
25
  process_stderr = ''
26
26
  line = ''
27
- until line =~ /worker\=0 ready/
27
+ until line.include?('worker=0 ready')
28
28
  line = error.gets
29
29
  raise process_stderr if line.nil?
30
30
  process_stderr << line
@@ -10,7 +10,7 @@ module Excon
10
10
  open_process(RbConfig.ruby, '-S', 'rackup', '-s', 'webrick', '--host', host, '--port', port, app_str)
11
11
  process_stderr = ""
12
12
  line = ''
13
- until line =~ /HTTPServer#start/
13
+ until line.include?('HTTPServer#start')
14
14
  line = error.gets
15
15
  raise process_stderr if line.nil?
16
16
  process_stderr << line
@@ -42,7 +42,6 @@ module Excon
42
42
  if RUBY_PLATFORM == 'java'
43
43
  @pid, @write, @read, @error = IO.popen4(*args)
44
44
  else
45
- GC.disable if RUBY_VERSION < '1.9'
46
45
  @pid, @write, @read, @error = Open4.popen4(*args)
47
46
  end
48
47
  @started_at = Time.now
@@ -57,7 +56,6 @@ module Excon
57
56
  Process.kill('USR1', pid)
58
57
  else
59
58
  Process.kill(9, pid)
60
- GC.enable if RUBY_VERSION < '1.9'
61
59
  Process.wait(pid)
62
60
  end
63
61
 
@@ -75,7 +73,7 @@ module Excon
75
73
  while (line = lines.shift)
76
74
  case line
77
75
  when /(ERROR|Error)/
78
- unless line =~ /(null cert chain|did not return a certificate|SSL_read:: internal error)/
76
+ unless line.match?(/(null cert chain|did not return a certificate|SSL_read:: internal error)/)
79
77
  in_err = true
80
78
  puts
81
79
  end
@@ -34,7 +34,7 @@ module Excon
34
34
  end
35
35
 
36
36
  rescue => error
37
- @socket.close rescue nil if @socket
37
+ @socket&.close rescue nil
38
38
  raise error
39
39
  end
40
40
 
data/lib/excon/utils.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Excon
3
4
  module Utils
4
- extend self
5
+ module_function
5
6
 
6
- CONTROL = (0x0..0x1f).map {|c| c.chr }.join + "\x7f"
7
+ CONTROL = "#{(0x0..0x1f).map(&:chr).join}\u007F"
7
8
  DELIMS = '<>#%"'
8
9
  UNWISE = '{}|\\^[]`'
9
- NONASCII = (0x80..0xff).map {|c| c.chr }.join
10
- UNESCAPED = /([#{ Regexp.escape(CONTROL + ' ' + DELIMS + UNWISE + NONASCII) }])/
11
- ESCAPED = /%([0-9a-fA-F]{2})/
10
+ NONASCII = (0x80..0xff).map(&:chr).join
11
+ UNESCAPED = /([#{Regexp.escape("#{CONTROL} #{DELIMS}#{UNWISE}#{NONASCII}")}])/.freeze
12
+ ESCAPED = /%([0-9a-fA-F]{2})/.freeze
12
13
 
13
14
  def binary_encode(string)
14
15
  if FORCE_ENC && string.encoding != Encoding::ASCII_8BIT
@@ -23,9 +24,8 @@ module Excon
23
24
  end
24
25
 
25
26
  def connection_uri(datum = @data)
26
- unless datum
27
- raise ArgumentError, '`datum` must be given unless called on a Connection'
28
- end
27
+ raise ArgumentError, '`datum` must be given unless called on a Connection' unless datum
28
+
29
29
  if datum[:scheme] == UNIX
30
30
  "#{datum[:scheme]}://#{datum[:socket]}"
31
31
  else
@@ -36,21 +36,15 @@ module Excon
36
36
  # Redact sensitive info from provided data
37
37
  def redact(datum)
38
38
  datum = datum.dup
39
- if datum.has_key?(:headers)
40
- if datum[:headers].has_key?('Authorization') || datum[:headers].has_key?('Proxy-Authorization')
39
+ if datum.key?(:headers)
40
+ if datum[:headers].key?('Authorization') || datum[:headers].key?('Proxy-Authorization')
41
41
  datum[:headers] = datum[:headers].dup
42
42
  end
43
- if datum[:headers].has_key?('Authorization')
44
- datum[:headers]['Authorization'] = REDACTED
45
- end
46
- if datum[:headers].has_key?('Proxy-Authorization')
47
- datum[:headers]['Proxy-Authorization'] = REDACTED
48
- end
49
- end
50
- if datum.has_key?(:password)
51
- datum[:password] = REDACTED
43
+ datum[:headers]['Authorization'] = REDACTED if datum[:headers].key?('Authorization')
44
+ datum[:headers]['Proxy-Authorization'] = REDACTED if datum[:headers].key?('Proxy-Authorization')
52
45
  end
53
- if datum.has_key?(:proxy) && datum[:proxy] && datum[:proxy].has_key?(:password)
46
+ datum[:password] = REDACTED if datum.key?(:password)
47
+ if datum.key?(:proxy) && datum[:proxy]&.key?(:password)
54
48
  datum[:proxy] = datum[:proxy].dup
55
49
  datum[:proxy][:password] = REDACTED
56
50
  end
@@ -62,21 +56,27 @@ module Excon
62
56
  end
63
57
 
64
58
  def port_string(datum)
65
- if datum[:port].nil? || (datum[:omit_default_port] && ((datum[:scheme].casecmp('http') == 0 && datum[:port] == 80) || (datum[:scheme].casecmp('https') == 0 && datum[:port] == 443)))
66
- ''
59
+ if !default_port?(datum) || datum[:include_default_port] || !datum[:omit_default_port]
60
+ ":#{datum[:port]}"
67
61
  else
68
- ':' + datum[:port].to_s
62
+ ''
69
63
  end
70
64
  end
71
65
 
66
+ def default_port?(datum)
67
+ (!datum[:scheme]&.casecmp?('unix') && datum[:port].nil?) ||
68
+ (datum[:scheme]&.casecmp?('http') && datum[:port] == 80) ||
69
+ (datum[:scheme]&.casecmp?('https') && datum[:port] == 443)
70
+ end
71
+
72
72
  def query_string(datum)
73
- str = String.new
73
+ str = +''
74
74
  case datum[:query]
75
75
  when String
76
76
  str << '?' << datum[:query]
77
77
  when Hash
78
78
  str << '?'
79
- datum[:query].sort_by {|k,_| k.to_s }.each do |key, values|
79
+ datum[:query].sort_by { |k, _| k.to_s }.each do |key, values|
80
80
  key = CGI.escape(key.to_s)
81
81
  if values.nil?
82
82
  str << key << '&'
@@ -94,46 +94,49 @@ module Excon
94
94
  # Splits a header value +str+ according to HTTP specification.
95
95
  def split_header_value(str)
96
96
  return [] if str.nil?
97
+
97
98
  str = str.dup.strip
98
99
  str = binary_encode(str)
99
- str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",])+)
100
- (?:,\s*|\Z)'xn).flatten
100
+ str.scan(/\G((?:"(?:\\.|[^"])+?"|[^",])+)
101
+ (?:,\s*|\Z)/xn).flatten
101
102
  end
102
103
 
103
104
  # Escapes HTTP reserved and unwise characters in +str+
104
105
  def escape_uri(str)
105
106
  str = str.dup
106
107
  str = binary_encode(str)
107
- str.gsub(UNESCAPED) { "%%%02X" % $1[0].ord }
108
+ str.gsub(UNESCAPED) { format('%%%02X', ::Regexp.last_match(1)[0].ord) }
108
109
  end
109
110
 
110
111
  # Unescapes HTTP reserved and unwise characters in +str+
111
112
  def unescape_uri(str)
112
113
  str = str.dup
113
114
  str = binary_encode(str)
114
- str.gsub(ESCAPED) { $1.hex.chr }
115
+ str.gsub(ESCAPED) { ::Regexp.last_match(1).hex.chr }
115
116
  end
116
117
 
117
118
  # Unescape form encoded values in +str+
118
119
  def unescape_form(str)
119
120
  str = str.dup
120
121
  str = binary_encode(str)
121
- str.gsub!(/\+/, ' ')
122
- str.gsub(ESCAPED) { $1.hex.chr }
122
+ str.tr!('+', ' ')
123
+ str.gsub(ESCAPED) { ::Regexp.last_match(1).hex.chr }
123
124
  end
124
125
 
125
126
  # Performs validation on the passed header hash and returns a string representation of the headers
126
127
  def headers_hash_to_s(headers)
127
- headers_str = String.new
128
+ headers_str = +''
128
129
  headers.each do |key, values|
129
- if key.to_s.match(/[\r\n]/)
130
- raise Excon::Errors::InvalidHeaderKey.new(key.to_s.inspect + ' contains forbidden "\r" or "\n"')
130
+ if key.to_s.match?(/[\r\n]/)
131
+ raise Excon::Errors::InvalidHeaderKey, "#{key.to_s.inspect} contains forbidden \"\\r\" or \"\\n\""
131
132
  end
133
+
132
134
  [values].flatten.each do |value|
133
- if value.to_s.match(/[\r\n]/)
135
+ if value.to_s.match?(/[\r\n]/)
134
136
  # Don't include the potentially sensitive header value (i.e. authorization token) in the message
135
- raise Excon::Errors::InvalidHeaderValue.new(key.to_s + ' header value contains forbidden "\r" or "\n"')
137
+ raise Excon::Errors::InvalidHeaderValue, "#{key} header value contains forbidden \"\\r\" or \"\\n\""
136
138
  end
139
+
137
140
  headers_str << key.to_s << ': ' << value.to_s << CR_NL
138
141
  end
139
142
  end
data/lib/excon/version.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Excon
3
- VERSION = '0.99.0'
4
+ VERSION = '1.2.5'
4
5
  end