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.
- checksums.yaml +4 -4
- data/README.md +24 -2
- data/data/cacert.pem +545 -275
- data/excon.gemspec +24 -17
- data/lib/excon/connection.rb +27 -14
- data/lib/excon/constants.rb +122 -113
- data/lib/excon/error.rb +2 -2
- data/lib/excon/middlewares/capture_cookies.rb +1 -1
- data/lib/excon/middlewares/decompress.rb +17 -22
- data/lib/excon/middlewares/redirect_follower.rb +3 -3
- data/lib/excon/response.rb +4 -4
- data/lib/excon/socket.rb +115 -25
- data/lib/excon/ssl_socket.rb +3 -2
- data/lib/excon/test/plugin/server/puma.rb +1 -1
- data/lib/excon/test/plugin/server/unicorn.rb +5 -5
- data/lib/excon/test/plugin/server/webrick.rb +1 -1
- data/lib/excon/test/server.rb +1 -3
- data/lib/excon/unix_socket.rb +1 -1
- data/lib/excon/utils.rb +39 -36
- data/lib/excon/version.rb +2 -1
- data/lib/excon.rb +39 -47
- metadata +49 -20
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
|
-
|
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
|
74
|
+
if @nonblock
|
66
75
|
result = String.new
|
67
|
-
block =
|
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
|
76
|
-
|
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
|
-
|
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
|
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
|
-
|
191
|
-
|
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 ||
|
199
|
-
|
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
|
-
|
203
|
-
@read_buffer
|
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
|
-
|
272
|
+
if max_length
|
224
273
|
if @read_buffer.empty?
|
225
|
-
|
274
|
+
# EOF met at beginning
|
275
|
+
@eof = @backend_eof
|
276
|
+
nil
|
226
277
|
else
|
227
|
-
@
|
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
|
-
|
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
|
-
|
234
|
-
|
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,
|
377
|
+
IO.select([socket], nil, nil, timeout)
|
300
378
|
when :connect_write
|
301
|
-
IO.select(nil, [socket], nil,
|
379
|
+
IO.select(nil, [socket], nil, timeout)
|
302
380
|
when :read
|
303
|
-
IO.select([socket], nil, nil,
|
381
|
+
IO.select([socket], nil, nil, timeout)
|
304
382
|
when :write
|
305
|
-
IO.select(nil, [socket], nil,
|
383
|
+
IO.select(nil, [socket], nil, timeout)
|
306
384
|
end
|
307
|
-
|
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
|
data/lib/excon/ssl_socket.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
13
|
+
until line.include?('HTTPServer#start')
|
14
14
|
line = error.gets
|
15
15
|
raise process_stderr if line.nil?
|
16
16
|
process_stderr << line
|
data/lib/excon/test/server.rb
CHANGED
@@ -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
|
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
|
data/lib/excon/unix_socket.rb
CHANGED
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
|
-
|
5
|
+
module_function
|
5
6
|
|
6
|
-
CONTROL = (0x0..0x1f).map
|
7
|
+
CONTROL = "#{(0x0..0x1f).map(&:chr).join}\u007F"
|
7
8
|
DELIMS = '<>#%"'
|
8
9
|
UNWISE = '{}|\\^[]`'
|
9
|
-
NONASCII = (0x80..0xff).map
|
10
|
-
UNESCAPED = /([#{
|
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
|
-
|
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.
|
40
|
-
if datum[:headers].
|
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].
|
44
|
-
|
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
|
-
|
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
|
66
|
-
|
59
|
+
if !default_port?(datum) || datum[:include_default_port] || !datum[:omit_default_port]
|
60
|
+
":#{datum[:port]}"
|
67
61
|
else
|
68
|
-
'
|
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 =
|
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(
|
100
|
-
(?:,\s*|\Z)
|
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) {
|
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) {
|
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.
|
122
|
-
str.gsub(ESCAPED) {
|
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 =
|
128
|
+
headers_str = +''
|
128
129
|
headers.each do |key, values|
|
129
|
-
if key.to_s.match(/[\r\n]/)
|
130
|
-
raise Excon::Errors::InvalidHeaderKey
|
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
|
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