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 +4 -4
- data/README.md +15 -2
- data/lib/excon/connection.rb +9 -0
- data/lib/excon/constants.rb +3 -0
- data/lib/excon/resolver_factory.rb +18 -0
- data/lib/excon/socket.rb +25 -41
- data/lib/excon/socks5.rb +194 -0
- data/lib/excon/socks5_socket.rb +36 -0
- data/lib/excon/socks5_ssl_socket.rb +44 -0
- data/lib/excon/test/plugin/server/webrick.rb +1 -1
- data/lib/excon/version.rb +1 -1
- data/lib/excon.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3df399d0b27add472158c8762cd3287e085e8f081705ac1fe48d88f54bd86346
|
|
4
|
+
data.tar.gz: 9021bb3bec4b54c8ef6c9a7d651ca474b55804b8f9509a826cd3ad699d23424f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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/
|
|
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/lib/excon/connection.rb
CHANGED
|
@@ -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
|
data/lib/excon/constants.rb
CHANGED
|
@@ -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] ||
|
|
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 =>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 =>
|
|
303
|
-
if
|
|
304
|
-
|
|
305
|
-
|
|
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 =>
|
|
331
|
-
if error.is_a?(OpenSSL::SSL::SSLError) &&
|
|
332
|
-
|
|
333
|
-
|
|
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 =>
|
|
352
|
-
if
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
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.
|
data/lib/excon/socks5.rb
ADDED
|
@@ -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?('
|
|
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
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.
|
|
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:
|
|
285
|
+
rubygems_version: 4.0.3
|
|
282
286
|
specification_version: 4
|
|
283
287
|
summary: speed, persistence, http(s)
|
|
284
288
|
test_files: []
|