excon 0.62.0 → 0.63.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of excon might be problematic. Click here for more details.

Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.github/stale.yml +17 -0
  3. data/.travis.yml +7 -19
  4. data/LICENSE.md +1 -1
  5. data/README.md +5 -4
  6. data/changelog.txt +25 -0
  7. data/data/cacert.pem +440 -994
  8. data/excon.gemspec +9 -0
  9. data/lib/excon.rb +9 -1
  10. data/lib/excon/connection.rb +52 -35
  11. data/lib/excon/constants.rb +33 -13
  12. data/lib/excon/error.rb +3 -0
  13. data/lib/excon/instrumentors/logging_instrumentor.rb +3 -14
  14. data/lib/excon/instrumentors/standard_instrumentor.rb +1 -8
  15. data/lib/excon/middlewares/base.rb +6 -0
  16. data/lib/excon/middlewares/expects.rb +6 -0
  17. data/lib/excon/middlewares/idempotent.rb +20 -3
  18. data/lib/excon/middlewares/instrumentor.rb +8 -0
  19. data/lib/excon/middlewares/mock.rb +8 -0
  20. data/lib/excon/middlewares/response_parser.rb +3 -0
  21. data/lib/excon/pretty_printer.rb +1 -8
  22. data/lib/excon/socket.rb +36 -10
  23. data/lib/excon/ssl_socket.rb +7 -0
  24. data/lib/excon/utils.rb +23 -4
  25. data/lib/excon/version.rb +1 -1
  26. data/spec/excon/test/server_spec.rb +2 -2
  27. data/spec/helpers/warning_helpers.rb +9 -0
  28. data/spec/requests/unix_socket_spec.rb +2 -10
  29. data/spec/requests/validation_spec.rb +80 -0
  30. data/spec/spec_helper.rb +2 -0
  31. data/spec/support/shared_contexts/test_stub_context.rb +11 -0
  32. data/spec/support/shared_examples/shared_example_for_clients.rb +6 -4
  33. data/tests/authorization_header_tests.rb +19 -21
  34. data/tests/bad_tests.rb +22 -0
  35. data/tests/batch_requests.rb +1 -1
  36. data/tests/complete_responses.rb +1 -1
  37. data/tests/data/127.0.0.1.cert.crt +15 -18
  38. data/tests/data/127.0.0.1.cert.key +28 -27
  39. data/tests/data/excon.cert.crt +15 -18
  40. data/tests/data/excon.cert.key +28 -27
  41. data/tests/error_tests.rb +1 -1
  42. data/tests/instrumentors/logging_instrumentor_tests.rb +28 -0
  43. data/tests/middlewares/decompress_tests.rb +1 -1
  44. data/tests/middlewares/idempotent_tests.rb +56 -17
  45. data/tests/middlewares/mock_tests.rb +2 -2
  46. data/tests/pipeline_tests.rb +1 -1
  47. data/tests/request_tests.rb +5 -6
  48. data/tests/response_tests.rb +1 -1
  49. data/tests/servers/bad.rb +5 -0
  50. data/tests/servers/good.rb +0 -8
  51. data/tests/servers/good_ipv4.rb +8 -0
  52. data/tests/servers/good_ipv6.rb +8 -0
  53. data/tests/test_helper.rb +27 -36
  54. metadata +17 -5
@@ -2,6 +2,14 @@
2
2
  module Excon
3
3
  module Middleware
4
4
  class Instrumentor < Excon::Middleware::Base
5
+ def self.valid_parameter_keys
6
+ [
7
+ :logger,
8
+ :instrumentor,
9
+ :instrumentor_name
10
+ ]
11
+ end
12
+
5
13
  def error_call(datum)
6
14
  if datum.has_key?(:instrumentor)
7
15
  datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.error", :error => datum[:error]) do
@@ -2,6 +2,14 @@
2
2
  module Excon
3
3
  module Middleware
4
4
  class Mock < Excon::Middleware::Base
5
+ def self.valid_parameter_keys
6
+ [
7
+ :allow_unstubbed_requests,
8
+ :captures,
9
+ :mock
10
+ ]
11
+ end
12
+
5
13
  def request_call(datum)
6
14
  if datum[:mock]
7
15
  # convert File/Tempfile body to string before matching:
@@ -6,6 +6,9 @@ module Excon
6
6
  unless datum.has_key?(:response)
7
7
  datum = Excon::Response.parse(datum[:connection].send(:socket), datum)
8
8
  end
9
+ if datum.has_key?(:logger)
10
+ datum[:response][:logger] = datum[:logger]
11
+ end
9
12
  @stack.response_call(datum)
10
13
  end
11
14
  end
@@ -9,14 +9,7 @@ module Excon
9
9
  datum.delete(:connection)
10
10
  datum.delete(:stack)
11
11
 
12
- if datum.has_key?(:headers) && datum[:headers].has_key?('Authorization')
13
- datum[:headers] = datum[:headers].dup
14
- datum[:headers]['Authorization'] = REDACTED
15
- end
16
-
17
- if datum.has_key?(:password)
18
- datum[:password] = REDACTED
19
- end
12
+ datum = Utils.redact(datum)
20
13
  end
21
14
 
22
15
  indent += 2
@@ -7,6 +7,23 @@ module Excon
7
7
 
8
8
  attr_accessor :data
9
9
 
10
+ # read/write drawn from https://github.com/ruby-amqp/bunny/commit/75d9dd79551b31a5dd3d1254c537bad471f108cf
11
+ CONNECT_RETRY_EXCEPTION_CLASSES = if defined?(IO::EINPROGRESSWaitWritable) # Ruby >= 2.1
12
+ [Errno::EINPROGRESS, IO::EINPROGRESSWaitWritable]
13
+ else # Ruby <= 2.0
14
+ [Errno::EINPROGRESS]
15
+ end
16
+ READ_RETRY_EXCEPTION_CLASSES = if defined?(IO::EAGAINWaitReadable) # Ruby >= 2.1
17
+ [Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable, IO::EAGAINWaitReadable, IO::EWOULDBLOCKWaitReadable]
18
+ else # Ruby <= 2.0
19
+ [Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable]
20
+ end
21
+ WRITE_RETRY_EXCEPTION_CLASSES = if defined?(IO::EAGAINWaitWritable) # Ruby >= 2.1
22
+ [Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable, IO::EAGAINWaitWritable, IO::EWOULDBLOCKWaitWritable]
23
+ else # Ruby <= 2.0
24
+ [Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable]
25
+ end
26
+
10
27
  def params
11
28
  Excon.display_warning('Excon::Socket#params is deprecated use Excon::Socket#data instead.')
12
29
  @data
@@ -46,7 +63,7 @@ module Excon
46
63
  begin
47
64
  buffer << @socket.read_nonblock(1) while buffer[-1] != "\n"
48
65
  buffer
49
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
66
+ rescue *READ_RETRY_EXCEPTION_CLASSES
50
67
  select_with_timeout(@socket, :read) && retry
51
68
  rescue OpenSSL::SSL::SSLError => error
52
69
  if error.message == 'read would block'
@@ -106,7 +123,7 @@ module Excon
106
123
  if @socket
107
124
  break
108
125
  end
109
-
126
+
110
127
  @remote_ip = ip
111
128
 
112
129
  # nonblocking connect
@@ -128,7 +145,7 @@ module Excon
128
145
  socket.connect(sockaddr)
129
146
  end
130
147
  @socket = socket
131
- rescue Errno::EINPROGRESS
148
+ rescue *CONNECT_RETRY_EXCEPTION_CLASSES
132
149
  select_with_timeout(socket, :connect_write)
133
150
  begin
134
151
  socket.connect_nonblock(sockaddr)
@@ -151,6 +168,17 @@ module Excon
151
168
  ::Socket::TCP_NODELAY,
152
169
  true)
153
170
  end
171
+
172
+ if @data[:keepalive]
173
+ if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| ::Socket.const_defined? c}
174
+ @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
175
+ @socket.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPIDLE, @data[:keepalive][:time])
176
+ @socket.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPINTVL, @data[:keepalive][:intvl])
177
+ @socket.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPCNT, @data[:keepalive][:probes])
178
+ else
179
+ Excon.display_warning('Excon::Socket keepalive was set, but is not supported by Ruby version.')
180
+ end
181
+ end
154
182
  end
155
183
 
156
184
  def read_nonblock(max_length)
@@ -170,7 +198,7 @@ module Excon
170
198
  else
171
199
  raise(error)
172
200
  end
173
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
201
+ rescue *READ_RETRY_EXCEPTION_CLASSES
174
202
  if @read_buffer.empty?
175
203
  # if we didn't read anything, try again...
176
204
  select_with_timeout(@socket, :read) && retry
@@ -199,7 +227,7 @@ module Excon
199
227
  else
200
228
  raise(error)
201
229
  end
202
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
230
+ rescue *READ_RETRY_EXCEPTION_CLASSES
203
231
  if @read_buffer.empty?
204
232
  select_with_timeout(@socket, :read) && retry
205
233
  end
@@ -208,9 +236,7 @@ module Excon
208
236
  end
209
237
 
210
238
  def write_nonblock(data)
211
- if FORCE_ENC
212
- data.force_encoding('BINARY')
213
- end
239
+ binary_encode(data)
214
240
  loop do
215
241
  written = nil
216
242
  begin
@@ -225,7 +251,7 @@ module Excon
225
251
  else
226
252
  raise error
227
253
  end
228
- rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => error
254
+ rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => error
229
255
  if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
230
256
  raise error
231
257
  else
@@ -246,7 +272,7 @@ module Excon
246
272
 
247
273
  def write_block(data)
248
274
  @socket.write(data)
249
- rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => error
275
+ rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => error
250
276
  if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
251
277
  raise error
252
278
  else
@@ -27,6 +27,13 @@ module Excon
27
27
  if @data[:ssl_version]
28
28
  ssl_context.ssl_version = @data[:ssl_version]
29
29
  end
30
+ if @data[:ssl_min_version]
31
+ ssl_context.min_version = @data[:ssl_min_version]
32
+ end
33
+ if @data[:ssl_max_version]
34
+ ssl_context.max_version = @data[:ssl_max_version]
35
+ end
36
+
30
37
 
31
38
  if @data[:ssl_verify_peer]
32
39
  # turn verification on
@@ -10,6 +10,12 @@ module Excon
10
10
  UNESCAPED = /([#{ Regexp.escape(CONTROL + ' ' + DELIMS + UNWISE + NONASCII) }])/
11
11
  ESCAPED = /%([0-9a-fA-F]{2})/
12
12
 
13
+ def binary_encode(string)
14
+ if FORCE_ENC && string.encoding != Encoding::ASCII_8BIT
15
+ string.force_encoding('BINARY')
16
+ end
17
+ end
18
+
13
19
  def connection_uri(datum = @data)
14
20
  unless datum
15
21
  raise ArgumentError, '`datum` must be given unless called on a Connection'
@@ -21,6 +27,19 @@ module Excon
21
27
  end
22
28
  end
23
29
 
30
+ # Redact sensitive info from provided data
31
+ def redact(datum)
32
+ datum = datum.dup
33
+ if datum.has_key?(:headers) && datum[:headers].has_key?('Authorization')
34
+ datum[:headers] = datum[:headers].dup
35
+ datum[:headers]['Authorization'] = REDACTED
36
+ end
37
+ if datum.has_key?(:password)
38
+ datum[:password] = REDACTED
39
+ end
40
+ datum
41
+ end
42
+
24
43
  def request_uri(datum)
25
44
  connection_uri(datum) + datum[:path] + query_string(datum)
26
45
  end
@@ -59,7 +78,7 @@ module Excon
59
78
  def split_header_value(str)
60
79
  return [] if str.nil?
61
80
  str = str.dup.strip
62
- str.force_encoding('BINARY') if FORCE_ENC
81
+ binary_encode(str)
63
82
  str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+)
64
83
  (?:,\s*|\Z)'xn).flatten
65
84
  end
@@ -67,21 +86,21 @@ module Excon
67
86
  # Escapes HTTP reserved and unwise characters in +str+
68
87
  def escape_uri(str)
69
88
  str = str.dup
70
- str.force_encoding('BINARY') if FORCE_ENC
89
+ binary_encode(str)
71
90
  str.gsub(UNESCAPED) { "%%%02X" % $1[0].ord }
72
91
  end
73
92
 
74
93
  # Unescapes HTTP reserved and unwise characters in +str+
75
94
  def unescape_uri(str)
76
95
  str = str.dup
77
- str.force_encoding('BINARY') if FORCE_ENC
96
+ binary_encode(str)
78
97
  str.gsub(ESCAPED) { $1.hex.chr }
79
98
  end
80
99
 
81
100
  # Unescape form encoded values in +str+
82
101
  def unescape_form(str)
83
102
  str = str.dup
84
- str.force_encoding('BINARY') if FORCE_ENC
103
+ binary_encode(str)
85
104
  str.gsub!(/\+/, ' ')
86
105
  str.gsub(ESCAPED) { $1.hex.chr }
87
106
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Excon
3
- VERSION = '0.62.0'
3
+ VERSION = '0.63.0'
4
4
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Excon::Test::Server do
4
-
4
+
5
5
  context 'when the web server is webrick' do
6
6
  it_should_behave_like "a excon test server", :webrick, 'basic.ru'
7
7
  end
@@ -23,6 +23,6 @@ describe Excon::Test::Server do
23
23
  end
24
24
 
25
25
  context 'when the web server is a executable' do
26
- it_should_behave_like "a excon test server", :exec, 'good.rb'
26
+ it_should_behave_like "a excon test server", :exec, 'good_ipv4.rb'
27
27
  end
28
28
  end
@@ -0,0 +1,9 @@
1
+ def silence_warnings
2
+ orig_verbose = $VERBOSE
3
+ $VERBOSE = nil
4
+ Excon.set_raise_on_warnings!(false)
5
+ yield
6
+ ensure
7
+ $VERBOSE = orig_verbose
8
+ Excon.set_raise_on_warnings!(true)
9
+ end
@@ -1,25 +1,17 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Excon::Connection do
4
+ include_context('stubs')
4
5
  context "when speaking to a UNIX socket" do
5
6
  context "Host header handling" do
6
7
  before do
7
- responder = ->(req) do
8
+ Excon.stub do |req|
8
9
  {
9
10
  body: req[:headers].to_json,
10
11
  status: 200,
11
12
  }
12
13
  end
13
-
14
- @original_mock = Excon.defaults[:mock]
15
- Excon.defaults[:mock] = true
16
- Excon.stub({}, responder)
17
- end
18
-
19
- after do
20
- Excon.defaults[:mock] = @original_mock
21
14
  end
22
-
23
15
  it "sends an empty Host= by default" do
24
16
  conn = Excon::Connection.new(
25
17
  scheme: "unix",
@@ -0,0 +1,80 @@
1
+ describe Excon::Connection do
2
+ include_context('stubs')
3
+ describe 'validating parameters' do
4
+ class FooMiddleware < Excon::Middleware::Base
5
+ def self.valid_parameter_keys
6
+ [:foo]
7
+ end
8
+ end
9
+
10
+ let(:foo_stack) do
11
+ Excon.defaults[:middlewares] + [FooMiddleware]
12
+ end
13
+
14
+ def expect_parameter_warning(validation, key)
15
+ expect { yield }.to raise_error(Excon::Error::Warning, "Invalid Excon #{validation} keys: #{key.inspect}")
16
+ end
17
+
18
+ context 'with default middleware' do
19
+ it 'Connection.new warns on invalid parameter keys' do
20
+ expect_parameter_warning('connection', :foo) do
21
+ Excon.new('http://foo', :foo => :bar)
22
+ end
23
+ end
24
+
25
+ it 'Connection#request warns on invalid parameter keys' do
26
+ conn = Excon.new('http://foo')
27
+ expect_parameter_warning('request', :foo) do
28
+ conn.request(:foo => :bar)
29
+ end
30
+ end
31
+ end
32
+
33
+ context 'with custom middleware at instantiation' do
34
+ it 'Connection.new accepts parameters that are valid for the provided middleware' do
35
+ Excon.new('http://foo', :foo => :bar, :middlewares => foo_stack)
36
+ end
37
+
38
+ it 'Connection.new warns on parameters that are not valid for the provided middleware' do
39
+ expect_parameter_warning('connection', :bar) do
40
+ Excon.new('http://foo', :bar => :baz, :middlewares => foo_stack)
41
+ end
42
+ end
43
+
44
+ it 'Connection#request accepts parameters that are valid for the provided middleware' do
45
+ Excon.stub({}, {})
46
+ conn = Excon.new('http://foo', :middlewares => foo_stack)
47
+ conn.request(:foo => :bar)
48
+ end
49
+
50
+ it 'Connection#request warns on parameters that are not valid for the provided middleware' do
51
+ conn = Excon.new('http://foo', :middlewares => foo_stack)
52
+ expect_parameter_warning('request', :bar) do
53
+ conn.request(:bar => :baz)
54
+ end
55
+ end
56
+ end
57
+
58
+ context 'with custom middleware at request time' do
59
+ it 'Connection#request accepts parameters that are valid for the provided middleware' do
60
+ Excon.stub({}, {})
61
+ conn = Excon.new('http://foo')
62
+ conn.request(:foo => :bar, :middlewares => foo_stack)
63
+ end
64
+
65
+ it 'Connection#request warns on parameters that are not valid for the request middleware' do
66
+ conn = Excon.new('http://foo')
67
+ expect_parameter_warning('request', :bar) do
68
+ conn.request(:bar => :baz, :middlewares => foo_stack)
69
+ end
70
+ end
71
+
72
+ it 'Connection#request warns on parameters from instantiation that are not valid for the request middleware' do
73
+ conn = Excon.new('http://foo', :foo => :bar, :middlewares => foo_stack)
74
+ expect_parameter_warning('connection', :foo) do
75
+ conn.request(:middlewares => Excon.defaults[:middlewares])
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -15,6 +15,8 @@ RSpec.configure do |config|
15
15
  if config.files_to_run.one?
16
16
  config.default_formatter = 'doc'
17
17
  end
18
+
19
+ Excon.set_raise_on_warnings!(true)
18
20
  end
19
21
 
20
22
  # Load helpers
@@ -0,0 +1,11 @@
1
+ shared_context "stubs" do
2
+ before do
3
+ @original_mock = Excon.defaults[:mock]
4
+ Excon.defaults[:mock] = true
5
+ end
6
+
7
+ after do
8
+ Excon.defaults[:mock] = @original_mock
9
+ Excon.stubs.clear
10
+ end
11
+ end
@@ -91,8 +91,10 @@ shared_examples_for 'a basic client' do |url = 'http://127.0.0.1:9292', opts = {
91
91
  data = []
92
92
  it 'yields with a chunk, remaining length, and total length' do
93
93
  expect do
94
- conn.request(method: :get, path: '/content-length/100') do |chunk, remaining_length, total_length|
95
- data = [chunk, remaining_length, total_length]
94
+ silence_warnings do
95
+ conn.request(method: :get, path: '/content-length/100') do |chunk, remaining_length, total_length|
96
+ data = [chunk, remaining_length, total_length]
97
+ end
96
98
  end
97
99
  end.to_not raise_error
98
100
  end
@@ -163,8 +165,8 @@ shared_examples_for 'a basic client' do |url = 'http://127.0.0.1:9292', opts = {
163
165
  end
164
166
 
165
167
  context 'when a string is the body paramter' do
166
- it 'does not change the econding of the body' do
167
- skip unless RUBY_VERSION >= '1.9'
168
+ it 'does not change the enconding of the body' do
169
+ skip unless RUBY_VERSION >= '2.0'
168
170
 
169
171
  string_body = '¥£€'
170
172
  expect do
@@ -1,29 +1,27 @@
1
1
  Shindo.tests('Excon basics (Authorization data redacted)') do
2
- with_rackup('basic_auth.ru') do
3
- cases = [
4
- ['user & pass', 'http://user1:pass1@foo.com/', 'Basic dXNlcjE6cGFzczE='],
5
- ['email & pass', 'http://foo%40bar.com:pass1@foo.com/', 'Basic Zm9vQGJhci5jb206cGFzczE='],
6
- ['user no pass', 'http://three_user@foo.com/', 'Basic dGhyZWVfdXNlcjo='],
7
- ['pass no user', 'http://:derppass@foo.com/', 'Basic OmRlcnBwYXNz']
8
- ]
9
- cases.each do |desc,url,auth_header|
10
- conn = nil
2
+ cases = [
3
+ ['user & pass', 'http://user1:pass1@foo.com/', 'Basic dXNlcjE6cGFzczE='],
4
+ ['email & pass', 'http://foo%40bar.com:pass1@foo.com/', 'Basic Zm9vQGJhci5jb206cGFzczE='],
5
+ ['user no pass', 'http://three_user@foo.com/', 'Basic dGhyZWVfdXNlcjo='],
6
+ ['pass no user', 'http://:derppass@foo.com/', 'Basic OmRlcnBwYXNz']
7
+ ]
8
+ cases.each do |desc,url,auth_header|
9
+ conn = nil
11
10
 
12
- test("authorization header concealed for #{desc}") do
13
- conn = Excon.new(url)
14
- !conn.inspect.include?(auth_header)
15
- end
16
-
17
- if conn.data[:password]
18
- test("password param concealed for #{desc}") do
19
- !conn.inspect.include?(conn.data[:password])
20
- end
21
- end
11
+ test("authorization header concealed for #{desc}") do
12
+ conn = Excon.new(url)
13
+ !conn.inspect.include?(auth_header)
14
+ end
22
15
 
23
- test("password param remains correct for #{desc}") do
24
- conn.data[:password] == URI.parse(url).password
16
+ if conn.data[:password]
17
+ test("password param concealed for #{desc}") do
18
+ !conn.inspect.include?(conn.data[:password])
25
19
  end
20
+ end
26
21
 
22
+ test("password param remains correct for #{desc}") do
23
+ conn.data[:password] == URI.parse(url).password
27
24
  end
25
+
28
26
  end
29
27
  end