excon 1.3.1 → 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: 8124592e1f6c3ea37d99a51f28930c5adfbd4a6f125c82b1dfd412e96d0398f9
4
- data.tar.gz: 2b94bf542e90fee28b76fd70aedcabfad6b96610922e5300fa437c550eec89fc
3
+ metadata.gz: 3df399d0b27add472158c8762cd3287e085e8f081705ac1fe48d88f54bd86346
4
+ data.tar.gz: 9021bb3bec4b54c8ef6c9a7d651ca474b55804b8f9509a826cd3ad699d23424f
5
5
  SHA512:
6
- metadata.gz: d8565b4788beb8dd5b1a0aaeea8cf4ba665447e27e2f6fe871a738a22bc0d54794a1b29c67de16b274b0d0e801034127afb9410c66460ea588187b8da0a42c96
7
- data.tar.gz: e54e47995333604a5cd33b803b9060e1ba51cbad20e472a7160444549397518a7feceb4d7ea548a715bf1503ae427f7fae96da02c2441965cf8f2bdaadebc2df
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.
data/data/cacert.pem CHANGED
@@ -1,7 +1,7 @@
1
1
  ##
2
2
  ## Bundle of CA Root Certificates
3
3
  ##
4
- ## Certificate data from Mozilla as of: Tue Nov 4 04:12:02 2025 GMT
4
+ ## Certificate data from Mozilla as of: Tue Dec 2 04:12:02 2025 GMT
5
5
  ##
6
6
  ## Find updated versions here: https://curl.se/docs/caextract.html
7
7
  ##
@@ -15,8 +15,8 @@
15
15
  ## an Apache+mod_ssl webserver for SSL client authentication.
16
16
  ## Just configure this file as the SSLCACertificateFile.
17
17
  ##
18
- ## Conversion done with mk-ca-bundle.pl version 1.29.
19
- ## SHA256: 039132bff5179ce57cec5803ba59fe37abe6d0297aeb538c5af27847f0702517
18
+ ## Conversion done with mk-ca-bundle.pl version 1.30.
19
+ ## SHA256: a903b3cd05231e39332515ef7ebe37e697262f39515a52015c23c62805b73cd0
20
20
  ##
21
21
 
22
22
 
@@ -3167,96 +3167,6 @@ bbd+NvBNEU/zy4k6LHiRUKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xk
3167
3167
  dUfFVZDj/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA==
3168
3168
  -----END CERTIFICATE-----
3169
3169
 
3170
- CommScope Public Trust ECC Root-01
3171
- ==================================
3172
- -----BEGIN CERTIFICATE-----
3173
- MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMwTjELMAkGA1UE
3174
- BhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1YmxpYyBUcnVz
3175
- dCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNaFw00NjA0MjgxNzM1NDJaME4xCzAJBgNVBAYT
3176
- AlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3Qg
3177
- RUNDIFJvb3QtMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16ocNfQj3Rid8NeeqrltqLx
3178
- eP0CflfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOcw5tjnSCDPgYLpkJEhRGnSjot
3179
- 6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
3180
- A1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggqhkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2
3181
- Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg2NED3W3ROT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liW
3182
- pDVfG2XqYZpwI7UNo5uSUm9poIyNStDuiw7LR47QjRE=
3183
- -----END CERTIFICATE-----
3184
-
3185
- CommScope Public Trust ECC Root-02
3186
- ==================================
3187
- -----BEGIN CERTIFICATE-----
3188
- MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMwTjELMAkGA1UE
3189
- BhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1YmxpYyBUcnVz
3190
- dCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRaFw00NjA0MjgxNzQ0NTNaME4xCzAJBgNVBAYT
3191
- AlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3Qg
3192
- RUNDIFJvb3QtMDIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l63FRD/cHB8o5mXxO1Q/M
3193
- MDALj2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/ubCK1sK9IRQq9qEmUv4RDsNuE
3194
- SgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
3195
- A1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggqhkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9
3196
- Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/nich/m35rChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs7
3197
- 3u1Z/GtMMH9ZzkXpc2AVmkzw5l4lIhVtwodZ0LKOag==
3198
- -----END CERTIFICATE-----
3199
-
3200
- CommScope Public Trust RSA Root-01
3201
- ==================================
3202
- -----BEGIN CERTIFICATE-----
3203
- MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQELBQAwTjELMAkG
3204
- A1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1YmxpYyBU
3205
- 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
- BAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1
3207
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- +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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
3227
- 6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw
3228
- -----END CERTIFICATE-----
3229
-
3230
- CommScope Public Trust RSA Root-02
3231
- ==================================
3232
- -----BEGIN CERTIFICATE-----
3233
- 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
- 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
- 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
- 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
- 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
- rCLENQE7xVTlWXZjpX/rwcRqmL0yjReA61260WI9JSMZNRTpf4mnG2I81lDnNJUDMrG0kyI9p+Kx
3239
- 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
- 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
- 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
- M9kzXzehxfCrPfp4sOcsn/Y+n2Dg70jpkEUeBVF4GiwSLFworA2iI540jwXmojPOEXcT1A6kHkIf
3243
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- MS/TiJVZbuXEs1DIFAhKm4sTg7GkcrI7djNB3NyqpgdvHSQSn8h2vS/ZjvQs7rfSOBAkNlEv41xd
3252
- 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
- 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
- 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
- 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
- iRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17Org3bhzjlP1v9mxnhMUF6cKojawHhRUzN
3257
- lM47ni3niAIi9G7oyOzWPPO5std3eqx7
3258
- -----END CERTIFICATE-----
3259
-
3260
3170
  Telekom Security TLS ECC Root 2020
3261
3171
  ==================================
3262
3172
  -----BEGIN CERTIFICATE-----
@@ -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.1'
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.1
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: []