excon 1.3.2 → 1.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5809aff164f1f2a113d10ec87f5ca1b1142244d2272347ff50247ad51adbbf5c
4
- data.tar.gz: 972b3bc18fbe4c2444a7e9811dbdb461f05fbd0fa4fd87e0c74752a10091add5
3
+ metadata.gz: 3df399d0b27add472158c8762cd3287e085e8f081705ac1fe48d88f54bd86346
4
+ data.tar.gz: 9021bb3bec4b54c8ef6c9a7d651ca474b55804b8f9509a826cd3ad699d23424f
5
5
  SHA512:
6
- metadata.gz: 930b75bd05ca0f8b9e37a973d0b257c78629d3c1b174382eb76f5772129c62979cbfe574aba8b85ce3bbd94f67a6bac7e32dfc4d7cc5483ed10f9f8888fb89b3
7
- data.tar.gz: dcc232afe8bce61c7bee396420e934ab14c83548662bc07cae0e8d04cc952024dd289da7a81635ea898ed59f712fa94604552f532f3d6a945dccd05f4e44a12c
6
+ metadata.gz: 240cc0b39f52ce4ad87dac7805038f05b27d8ba6ca61c05e7713dfe2a231eb259e8bc48d5a500e6040d9565fea70a0b6c1e7939b3b2da2c86a11e59b200678b3
7
+ data.tar.gz: 3960311628a4f2be6c43957de1e68d4e29f0b2ab290943eb1efd3ff21301d526780b4dd10fa16d5f0761510ba696f36f7f802653ba9f6468f845c6bcb4fb23d4
data/README.md CHANGED
@@ -213,6 +213,19 @@ dns_resolver = Resolv::DNS.new(nameserver: ['127.0.0.1'])
213
213
  dns_resolver.timeouts = 3
214
214
  resolver = Resolv.new([Resolv::Hosts.new, dns_resolver])
215
215
  connection = Excon.new('http://geemus.com', :resolv_resolver => resolver)
216
+
217
+ # As an global alternative for Excon, you can configure a custom resolver
218
+ # factory which produces new resolver instances, configured to your likings.
219
+ # This even works with Ruby Ractors!
220
+ class CustomResolverFactory
221
+ # @return [Resolv] the new resolver instance
222
+ def self.create_resolver
223
+ dns_resolver = Resolv::DNS.new(nameserver: ['127.0.0.1'])
224
+ dns_resolver.timeouts = 3
225
+ Resolv.new([Resolv::Hosts.new, dns_resolver])
226
+ end
227
+ end
228
+ Excon.defaults[:resolver_factory] = CustomResolverFactory
216
229
  ```
217
230
 
218
231
  ## Chunked Requests
@@ -311,10 +324,10 @@ s.close
311
324
  The Unix socket will work for one-off requests and multiuse connections. A Unix socket path must be provided separate from the resource path.
312
325
 
313
326
  ```ruby
314
- connection = Excon.new('unix:///', :socket => '/tmp/unicorn.sock')
327
+ connection = Excon.new('unix:///', :socket => '/tmp/puma.sock')
315
328
  connection.request(:method => :get, :path => '/ping')
316
329
 
317
- Excon.get('unix:///ping', :socket => '/tmp/unicorn.sock')
330
+ Excon.get('unix:///ping', :socket => '/tmp/puma.sock')
318
331
  ```
319
332
 
320
333
  NOTE: Proxies will be ignored when using a Unix socket, since a Unix socket has to be local.
@@ -60,6 +60,8 @@ module Excon
60
60
  # @option params [Fixnum] :retry_interval Set how long to wait between retries. (Default 0)
61
61
  # @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
62
62
  # @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
63
+ # @option params [Resolv, nil] :resolv_resolver A ready to use +Resolv+ resolver instance.
64
+ # @option params [Class] :resolver_factory Const of the resolver factory. Defaults to 'Excon::ResolverFactory'
63
65
  def initialize(params = {})
64
66
  @pid = Process.pid
65
67
  @data = Excon.defaults.dup
@@ -483,6 +485,13 @@ module Excon
483
485
  unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false
484
486
  sockets[@socket_key] ||= if datum[:scheme] == UNIX || unix_proxy
485
487
  Excon::UnixSocket.new(datum)
488
+ elsif datum[:socks5_proxy]
489
+ # SOCKS5 proxy - use appropriate socket based on target scheme
490
+ if datum[:ssl_uri_schemes].include?(datum[:scheme])
491
+ Excon::SOCKS5SSLSocket.new(datum)
492
+ else
493
+ Excon::SOCKS5Socket.new(datum)
494
+ end
486
495
  elsif datum[:ssl_uri_schemes].include?(datum[:scheme])
487
496
  Excon::SSLSocket.new(datum)
488
497
  else
@@ -60,6 +60,7 @@ module Excon
60
60
  read_timeout
61
61
  request_block
62
62
  resolv_resolver
63
+ resolver_factory
63
64
  response_block
64
65
  stubs
65
66
  timeout
@@ -97,6 +98,7 @@ module Excon
97
98
  proxy
98
99
  scheme
99
100
  socket
101
+ socks5_proxy
100
102
  ssl_ca_file
101
103
  ssl_ca_path
102
104
  ssl_cert_store
@@ -170,6 +172,7 @@ module Excon
170
172
  persistent: false,
171
173
  read_timeout: 60,
172
174
  resolv_resolver: nil,
175
+ resolver_factory: Excon::ResolverFactory,
173
176
  retry_errors: DEFAULT_RETRY_ERRORS,
174
177
  retry_limit: DEFAULT_RETRY_LIMIT,
175
178
  ssl_verify_peer: true,
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module Excon
3
+ # This factory produces new +resolv+ gem resolver instances. Users who wants
4
+ # to configure a custom resolver (varying settings for varying resolvers) can
5
+ # provide a custom resolver factory class and configure it globally on the
6
+ # Excon defaults:
7
+ #
8
+ # Excon.defaults[:resolver_factory] = MyCustomResolverFactory
9
+ #
10
+ # Then you just need to provide a static method called +.create_resolver+
11
+ # which returns a new +Resolv+ instance. This allows the customization.
12
+ class ResolverFactory
13
+ # @return [Resolv] the new resolver instance
14
+ def self.create_resolver
15
+ Resolv.new
16
+ end
17
+ end
18
+ end
data/lib/excon/socket.rb CHANGED
@@ -132,7 +132,7 @@ module Excon
132
132
  family = @data[:proxy][:family]
133
133
  end
134
134
 
135
- resolver = @data[:resolv_resolver] || Resolv.new
135
+ resolver = @data[:resolv_resolver] || @data[:resolver_factory].create_resolver
136
136
 
137
137
  # Deprecated
138
138
  if @data[:dns_timeouts]
@@ -252,15 +252,11 @@ module Excon
252
252
  end
253
253
  end
254
254
  end
255
- rescue OpenSSL::SSL::SSLError => error
256
- if error.message == 'read would block'
257
- if @read_buffer.empty?
258
- select_with_timeout(@socket, :read) && retry
259
- end
260
- else
261
- raise(error)
262
- end
263
- rescue *READ_RETRY_EXCEPTION_CLASSES
255
+ rescue OpenSSL::SSL::SSLError => e
256
+ raise(e) unless e.message == 'read would block'
257
+
258
+ select_with_timeout(@socket, :read) && retry if @read_buffer.empty?
259
+ rescue *READ_RETRY_EXCEPTION_CLASSES => e
264
260
  if @read_buffer.empty?
265
261
  # if we didn't read anything, try again...
266
262
  select_with_timeout(@socket, :read) && retry
@@ -299,12 +295,10 @@ module Excon
299
295
 
300
296
  def read_block(max_length)
301
297
  @socket.read(max_length)
302
- rescue OpenSSL::SSL::SSLError => error
303
- if error.message == 'read would block'
304
- select_with_timeout(@socket, :read) && retry
305
- else
306
- raise(error)
307
- end
298
+ rescue OpenSSL::SSL::SSLError => e
299
+ select_with_timeout(@socket, :read) && retry if e.message == 'read would block'
300
+
301
+ raise(error)
308
302
  rescue *READ_RETRY_EXCEPTION_CLASSES
309
303
  select_with_timeout(@socket, :read) && retry
310
304
  rescue EOFError
@@ -327,12 +321,10 @@ module Excon
327
321
  else
328
322
  raise error
329
323
  end
330
- rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => error
331
- if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
332
- raise error
333
- else
334
- select_with_timeout(@socket, :write) && retry
335
- end
324
+ rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => e
325
+ raise e if error.is_a?(OpenSSL::SSL::SSLError) && e.message != 'write would block'
326
+
327
+ select_with_timeout(@socket, :write) && retry
336
328
  end
337
329
 
338
330
  # Fast, common case.
@@ -348,12 +340,10 @@ module Excon
348
340
 
349
341
  def write_block(data)
350
342
  @socket.write(data)
351
- rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => error
352
- if error.is_a?(OpenSSL::SSL::SSLError) && error.message != 'write would block'
353
- raise error
354
- else
355
- select_with_timeout(@socket, :write) && retry
356
- end
343
+ rescue OpenSSL::SSL::SSLError, *WRITE_RETRY_EXCEPTION_CLASSES => e
344
+ raise e if e.is_a?(OpenSSL::SSL::SSLError) && e.message != 'write would block'
345
+
346
+ select_with_timeout(@socket, :write) && retry
357
347
  end
358
348
 
359
349
  def select_with_timeout(socket, type)
@@ -373,25 +363,19 @@ module Excon
373
363
  end
374
364
 
375
365
  select = case type
376
- when :connect_read
377
- IO.select([socket], nil, nil, timeout)
378
- when :connect_write
379
- IO.select(nil, [socket], nil, timeout)
380
- when :read
381
- IO.select([socket], nil, nil, timeout)
382
- when :write
383
- IO.select(nil, [socket], nil, timeout)
384
- end
366
+ when :connect_read, :read
367
+ IO.select([socket], nil, nil, timeout)
368
+ when :connect_write, :write
369
+ IO.select(nil, [socket], nil, timeout)
370
+ end
385
371
 
386
- select || raise(Excon::Errors::Timeout.new("#{timeout_kind} timeout reached"))
372
+ select || raise(Excon::Errors::Timeout.new, "#{timeout_kind} timeout reached")
387
373
  end
388
374
 
389
375
  def unpacked_sockaddr
390
376
  @unpacked_sockaddr ||= ::Socket.unpack_sockaddr_in(@socket.to_io.getsockname)
391
377
  rescue ArgumentError => e
392
- unless e.message == 'not an AF_INET/AF_INET6 sockaddr'
393
- raise
394
- end
378
+ raise unless e.message == 'not an AF_INET/AF_INET6 sockaddr'
395
379
  end
396
380
 
397
381
  # Returns the remaining time in seconds until we reach the deadline for the request timeout.
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ # SOCKS5 protocol implementation (RFC 1928, RFC 1929)
5
+ # Shared module for SOCKS5Socket and SOCKS5SSLSocket
6
+ module SOCKS5
7
+ SOCKS5_VERSION = 0x05
8
+ SOCKS5_RESERVED = 0x00
9
+
10
+ # Authentication methods
11
+ SOCKS5_NO_AUTH = 0x00
12
+ SOCKS5_AUTH_USERNAME_PASSWORD = 0x02
13
+ SOCKS5_NO_ACCEPTABLE_AUTH = 0xFF
14
+
15
+ # Commands
16
+ SOCKS5_CMD_CONNECT = 0x01
17
+
18
+ # Address types
19
+ SOCKS5_ATYP_IPV4 = 0x01
20
+ SOCKS5_ATYP_DOMAIN = 0x03
21
+ SOCKS5_ATYP_IPV6 = 0x04
22
+
23
+ # Reply codes
24
+ SOCKS5_SUCCESS = 0x00
25
+ SOCKS5_ERRORS = {
26
+ 0x01 => 'General SOCKS server failure',
27
+ 0x02 => 'Connection not allowed by ruleset',
28
+ 0x03 => 'Network unreachable',
29
+ 0x04 => 'Host unreachable',
30
+ 0x05 => 'Connection refused',
31
+ 0x06 => 'TTL expired',
32
+ 0x07 => 'Command not supported',
33
+ 0x08 => 'Address type not supported'
34
+ }.freeze
35
+
36
+ # Maximum hostname length per RFC 1928
37
+ MAX_HOSTNAME_LENGTH = 255
38
+
39
+ private
40
+
41
+ # Parse SOCKS5 proxy string into components
42
+ # @param proxy_string [String] Proxy specification in various formats
43
+ # @return [Array<String, String, String, String>] host, port, user, pass
44
+ def parse_socks5_proxy(proxy_string)
45
+ # Support formats:
46
+ # host:port
47
+ # user:pass@host:port
48
+ # socks5://host:port
49
+ # socks5://user:pass@host:port
50
+ proxy_string = proxy_string.to_s.sub(%r{^socks5://}, '')
51
+
52
+ user = nil
53
+ pass = nil
54
+
55
+ if proxy_string.include?('@')
56
+ auth, host_port = proxy_string.split('@', 2)
57
+ user, pass = auth.split(':', 2)
58
+ else
59
+ host_port = proxy_string
60
+ end
61
+
62
+ host, port = host_port.split(':', 2)
63
+ port ||= '1080'
64
+
65
+ [host, port, user, pass]
66
+ end
67
+
68
+ # Perform SOCKS5 authentication handshake
69
+ def socks5_authenticate
70
+ auth_methods = if @proxy_user && @proxy_pass
71
+ [SOCKS5_NO_AUTH, SOCKS5_AUTH_USERNAME_PASSWORD]
72
+ else
73
+ [SOCKS5_NO_AUTH]
74
+ end
75
+
76
+ greeting = [SOCKS5_VERSION, auth_methods.length, *auth_methods].pack('C*')
77
+ @socket.write(greeting)
78
+
79
+ response = socks5_read_exactly(2)
80
+ version, chosen_method = response.unpack('CC')
81
+
82
+ if version != SOCKS5_VERSION
83
+ raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy returned invalid version: #{version}"))
84
+ end
85
+
86
+ case chosen_method
87
+ when SOCKS5_NO_AUTH
88
+ # No authentication required
89
+ when SOCKS5_AUTH_USERNAME_PASSWORD
90
+ unless @proxy_user && @proxy_pass
91
+ raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy requires authentication but no credentials provided'))
92
+ end
93
+ socks5_username_password_auth
94
+ when SOCKS5_NO_ACCEPTABLE_AUTH
95
+ raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy: no acceptable authentication methods'))
96
+ else
97
+ raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy: unsupported authentication method #{chosen_method}"))
98
+ end
99
+ end
100
+
101
+ # RFC 1929: Username/Password Authentication
102
+ def socks5_username_password_auth
103
+ auth_request = [
104
+ 0x01, # auth protocol version
105
+ @proxy_user.bytesize,
106
+ @proxy_user,
107
+ @proxy_pass.bytesize,
108
+ @proxy_pass
109
+ ].pack('CCA*CA*')
110
+
111
+ @socket.write(auth_request)
112
+
113
+ response = socks5_read_exactly(2)
114
+ _, status = response.unpack('CC')
115
+
116
+ unless status == 0x00
117
+ raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy authentication failed'))
118
+ end
119
+ end
120
+
121
+ # Request connection to target through SOCKS5 proxy
122
+ def socks5_connect(host, port)
123
+ if host.bytesize > MAX_HOSTNAME_LENGTH
124
+ raise Excon::Error::Socket.new(Exception.new("SOCKS5: hostname exceeds maximum length of #{MAX_HOSTNAME_LENGTH} bytes"))
125
+ end
126
+
127
+ # Build CONNECT request with domain name (let proxy resolve DNS)
128
+ request = [SOCKS5_VERSION, SOCKS5_CMD_CONNECT, SOCKS5_RESERVED].pack('CCC')
129
+ request += [SOCKS5_ATYP_DOMAIN, host.bytesize, host].pack('CCA*')
130
+ request += [port.to_i].pack('n')
131
+
132
+ @socket.write(request)
133
+
134
+ response = socks5_read_exactly(4)
135
+ version, reply, _, atyp = response.unpack('CCCC')
136
+
137
+ if version != SOCKS5_VERSION
138
+ raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy returned invalid version: #{version}"))
139
+ end
140
+
141
+ unless reply == SOCKS5_SUCCESS
142
+ error_msg = SOCKS5_ERRORS[reply] || "Unknown error (#{reply})"
143
+ raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy connect failed: #{error_msg}"))
144
+ end
145
+
146
+ # Read and discard bound address (not needed for CONNECT)
147
+ socks5_read_bound_address(atyp)
148
+ end
149
+
150
+ def socks5_read_bound_address(atyp)
151
+ case atyp
152
+ when SOCKS5_ATYP_IPV4
153
+ socks5_read_exactly(4 + 2) # 4 bytes IP + 2 bytes port
154
+ when SOCKS5_ATYP_DOMAIN
155
+ domain_len = socks5_read_exactly(1).unpack1('C')
156
+ socks5_read_exactly(domain_len + 2)
157
+ when SOCKS5_ATYP_IPV6
158
+ socks5_read_exactly(16 + 2) # 16 bytes IP + 2 bytes port
159
+ else
160
+ raise Excon::Error::Socket.new(Exception.new("SOCKS5 proxy returned unknown address type: #{atyp}"))
161
+ end
162
+ end
163
+
164
+ # Read exact number of bytes with timeout support
165
+ def socks5_read_exactly(nbytes)
166
+ data = ''.dup
167
+ deadline = @data[:read_timeout] ? Time.now + @data[:read_timeout] : nil
168
+
169
+ while data.bytesize < nbytes
170
+ if deadline
171
+ remaining = deadline - Time.now
172
+ if remaining <= 0
173
+ raise Excon::Error::Timeout.new('SOCKS5 read timeout')
174
+ end
175
+ ready = IO.select([@socket], nil, nil, remaining)
176
+ unless ready
177
+ raise Excon::Error::Timeout.new('SOCKS5 read timeout')
178
+ end
179
+ end
180
+
181
+ chunk = @socket.read_nonblock(nbytes - data.bytesize, exception: false)
182
+ case chunk
183
+ when :wait_readable
184
+ IO.select([@socket], nil, nil, deadline ? [deadline - Time.now, 0].max : nil)
185
+ when nil, ''
186
+ raise Excon::Error::Socket.new(Exception.new('SOCKS5 proxy connection closed unexpectedly'))
187
+ else
188
+ data << chunk
189
+ end
190
+ end
191
+ data
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ class SOCKS5Socket < Socket
5
+ include SOCKS5
6
+
7
+ def initialize(data = {})
8
+ @socks5_proxy = data[:socks5_proxy]
9
+ @proxy_host, @proxy_port, @proxy_user, @proxy_pass = parse_socks5_proxy(@socks5_proxy)
10
+ super(data)
11
+ end
12
+
13
+ private
14
+
15
+ # Proxy-swap pattern: temporarily set @data[:proxy] to the SOCKS5 proxy
16
+ # so that Socket#connect routes the TCP connection there (inheriting DNS
17
+ # resolution, nonblock, retry, keepalive, reuseaddr, remote_ip tracking).
18
+ # After TCP is up, clear :proxy and run the SOCKS5 handshake.
19
+ def connect
20
+ @data[:proxy] = {
21
+ host: @proxy_host,
22
+ hostname: @proxy_host,
23
+ port: @proxy_port.to_i
24
+ }
25
+
26
+ begin
27
+ super
28
+ ensure
29
+ @data.delete(:proxy)
30
+ end
31
+
32
+ socks5_authenticate
33
+ socks5_connect(@data[:host], @data[:port])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ class SOCKS5SSLSocket < SSLSocket
5
+ include SOCKS5
6
+
7
+ def initialize(data = {})
8
+ @socks5_proxy = data[:socks5_proxy]
9
+ @proxy_host, @proxy_port, @proxy_user, @proxy_pass = parse_socks5_proxy(@socks5_proxy)
10
+ super(data)
11
+ end
12
+
13
+ private
14
+
15
+ # Proxy-swap pattern (same as SOCKS5Socket#connect).
16
+ #
17
+ # Call chain:
18
+ # SOCKS5SSLSocket#initialize -> super (SSLSocket#initialize)
19
+ # -> super (Socket#initialize) -> connect
20
+ # -> SOCKS5SSLSocket#connect (this method)
21
+ # -> super -> SSLSocket#connect -> Socket#connect (TCP to proxy)
22
+ # -> SOCKS5 handshake on raw TCP socket
23
+ # <- returns to SSLSocket#initialize
24
+ # -> @data[:proxy] is nil, so HTTP CONNECT is skipped (line 111)
25
+ # -> SSL wrapping on the SOCKS5-tunneled socket
26
+ def connect
27
+ @data[:proxy] = {
28
+ host: @proxy_host,
29
+ hostname: @proxy_host,
30
+ port: @proxy_port.to_i
31
+ }
32
+
33
+ begin
34
+ super
35
+ ensure
36
+ # Clear :proxy so SSLSocket#initialize skips HTTP CONNECT (line 111)
37
+ @data.delete(:proxy)
38
+ end
39
+
40
+ socks5_authenticate
41
+ socks5_connect(@data[:host], @data[:port])
42
+ end
43
+ end
44
+ end
@@ -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.include?('HTTPServer#start')
13
+ until line.include?('Server#start')
14
14
  line = error.gets
15
15
  raise process_stderr if line.nil?
16
16
  process_stderr << line
data/lib/excon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Excon
4
- VERSION = '1.3.2'
4
+ VERSION = '1.4.0'
5
5
  end
data/lib/excon.rb CHANGED
@@ -27,6 +27,7 @@ require 'excon/middlewares/mock'
27
27
  require 'excon/middlewares/response_parser'
28
28
 
29
29
  require 'excon/error'
30
+ require 'excon/resolver_factory'
30
31
  require 'excon/constants'
31
32
  require 'excon/utils'
32
33
 
@@ -39,6 +40,9 @@ require 'excon/middlewares/capture_cookies'
39
40
  require 'excon/pretty_printer'
40
41
  require 'excon/socket'
41
42
  require 'excon/ssl_socket'
43
+ require 'excon/socks5'
44
+ require 'excon/socks5_socket'
45
+ require 'excon/socks5_ssl_socket'
42
46
  require 'excon/instrumentors/standard_instrumentor'
43
47
  require 'excon/instrumentors/logging_instrumentor'
44
48
  require 'excon/unix_socket'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excon
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dpiddy (Dan Peterson)
@@ -241,8 +241,12 @@ files:
241
241
  - lib/excon/middlewares/redirect_follower.rb
242
242
  - lib/excon/middlewares/response_parser.rb
243
243
  - lib/excon/pretty_printer.rb
244
+ - lib/excon/resolver_factory.rb
244
245
  - lib/excon/response.rb
245
246
  - lib/excon/socket.rb
247
+ - lib/excon/socks5.rb
248
+ - lib/excon/socks5_socket.rb
249
+ - lib/excon/socks5_ssl_socket.rb
246
250
  - lib/excon/ssl_socket.rb
247
251
  - lib/excon/test/plugin/server/exec.rb
248
252
  - lib/excon/test/plugin/server/puma.rb
@@ -278,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
278
282
  - !ruby/object:Gem::Version
279
283
  version: '0'
280
284
  requirements: []
281
- rubygems_version: 3.6.9
285
+ rubygems_version: 4.0.3
282
286
  specification_version: 4
283
287
  summary: speed, persistence, http(s)
284
288
  test_files: []