excon 1.3.0 → 1.5.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/data/cacert.pem +66 -665
- data/lib/excon/connection.rb +17 -0
- data/lib/excon/constants.rb +3 -0
- data/lib/excon/error.rb +1 -0
- data/lib/excon/middlewares/redirect_follower.rb +16 -4
- 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 +5 -2
- metadata +6 -3
- data/lib/excon/middlewares/capture_cookies.rb +0 -32
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
|
|
@@ -100,6 +102,10 @@ module Excon
|
|
|
100
102
|
@socket_key = "#{@data[:scheme]}://#{@data[:socket]}"
|
|
101
103
|
end
|
|
102
104
|
else
|
|
105
|
+
if @data.key?(:host) && @data[:host].nil?
|
|
106
|
+
raise Excon::Error::InvalidParameter, "host is required for non-Unix connections; " \
|
|
107
|
+
"the URL may be malformed (e.g. missing \"//\" after the scheme)"
|
|
108
|
+
end
|
|
103
109
|
@socket_key = "#{@data[:scheme]}://#{@data[:host]}#{port_string(@data)}"
|
|
104
110
|
end
|
|
105
111
|
reset
|
|
@@ -254,6 +260,10 @@ module Excon
|
|
|
254
260
|
if datum[:scheme] == UNIX
|
|
255
261
|
datum[:headers][host_key] ||= ''
|
|
256
262
|
else
|
|
263
|
+
if datum[:host].nil?
|
|
264
|
+
raise Excon::Error::InvalidParameter, "host is required for non-Unix connections; " \
|
|
265
|
+
"the URL may be malformed (e.g. missing \"//\" after the scheme)"
|
|
266
|
+
end
|
|
257
267
|
datum[:headers][host_key] ||= datum[:host] + port_string(datum)
|
|
258
268
|
end
|
|
259
269
|
|
|
@@ -483,6 +493,13 @@ module Excon
|
|
|
483
493
|
unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false
|
|
484
494
|
sockets[@socket_key] ||= if datum[:scheme] == UNIX || unix_proxy
|
|
485
495
|
Excon::UnixSocket.new(datum)
|
|
496
|
+
elsif datum[:socks5_proxy]
|
|
497
|
+
# SOCKS5 proxy - use appropriate socket based on target scheme
|
|
498
|
+
if datum[:ssl_uri_schemes].include?(datum[:scheme])
|
|
499
|
+
Excon::SOCKS5SSLSocket.new(datum)
|
|
500
|
+
else
|
|
501
|
+
Excon::SOCKS5Socket.new(datum)
|
|
502
|
+
end
|
|
486
503
|
elsif datum[:ssl_uri_schemes].include?(datum[:scheme])
|
|
487
504
|
Excon::SSLSocket.new(datum)
|
|
488
505
|
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,
|
data/lib/excon/error.rb
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
module Excon
|
|
3
3
|
module Middleware
|
|
4
4
|
class RedirectFollower < Excon::Middleware::Base
|
|
5
|
+
REDACTION_HEADERS = %w[
|
|
6
|
+
Authorization
|
|
7
|
+
Cookie
|
|
8
|
+
Cookie2
|
|
9
|
+
Host
|
|
10
|
+
Proxy-Connection
|
|
11
|
+
Proxy-Authorization
|
|
12
|
+
X-API-Key
|
|
13
|
+
X-Auth-Token
|
|
14
|
+
X-CSRF-Token
|
|
15
|
+
X-Session-ID
|
|
16
|
+
]
|
|
17
|
+
|
|
5
18
|
def self.valid_parameter_keys
|
|
6
19
|
[
|
|
7
20
|
:redirects_remaining,
|
|
@@ -54,10 +67,9 @@ module Excon
|
|
|
54
67
|
params[:headers].delete('Content-Length')
|
|
55
68
|
end
|
|
56
69
|
params[:headers] = datum[:headers].dup
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
params[:headers].delete('Host')
|
|
70
|
+
REDACTION_HEADERS.each do |header|
|
|
71
|
+
params[:headers].delete(header)
|
|
72
|
+
end
|
|
61
73
|
params.merge!(
|
|
62
74
|
:scheme => uri.scheme || datum[:scheme],
|
|
63
75
|
:host => uri.host || datum[:host],
|
|
@@ -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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
$:.unshift(File.dirname(__FILE__)) unless
|
|
4
4
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
|
5
5
|
|
|
6
|
-
require 'cgi'
|
|
6
|
+
require 'cgi/escape'
|
|
7
7
|
require 'forwardable'
|
|
8
8
|
require 'ipaddr'
|
|
9
9
|
require 'openssl'
|
|
@@ -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
|
|
|
@@ -35,10 +36,12 @@ require 'excon/headers'
|
|
|
35
36
|
require 'excon/response'
|
|
36
37
|
require 'excon/middlewares/escape_path'
|
|
37
38
|
require 'excon/middlewares/redirect_follower'
|
|
38
|
-
require 'excon/middlewares/capture_cookies'
|
|
39
39
|
require 'excon/pretty_printer'
|
|
40
40
|
require 'excon/socket'
|
|
41
41
|
require 'excon/ssl_socket'
|
|
42
|
+
require 'excon/socks5'
|
|
43
|
+
require 'excon/socks5_socket'
|
|
44
|
+
require 'excon/socks5_ssl_socket'
|
|
42
45
|
require 'excon/instrumentors/standard_instrumentor'
|
|
43
46
|
require 'excon/instrumentors/logging_instrumentor'
|
|
44
47
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dpiddy (Dan Peterson)
|
|
@@ -231,7 +231,6 @@ files:
|
|
|
231
231
|
- lib/excon/instrumentors/logging_instrumentor.rb
|
|
232
232
|
- lib/excon/instrumentors/standard_instrumentor.rb
|
|
233
233
|
- lib/excon/middlewares/base.rb
|
|
234
|
-
- lib/excon/middlewares/capture_cookies.rb
|
|
235
234
|
- lib/excon/middlewares/decompress.rb
|
|
236
235
|
- lib/excon/middlewares/escape_path.rb
|
|
237
236
|
- lib/excon/middlewares/expects.rb
|
|
@@ -241,8 +240,12 @@ files:
|
|
|
241
240
|
- lib/excon/middlewares/redirect_follower.rb
|
|
242
241
|
- lib/excon/middlewares/response_parser.rb
|
|
243
242
|
- lib/excon/pretty_printer.rb
|
|
243
|
+
- lib/excon/resolver_factory.rb
|
|
244
244
|
- lib/excon/response.rb
|
|
245
245
|
- lib/excon/socket.rb
|
|
246
|
+
- lib/excon/socks5.rb
|
|
247
|
+
- lib/excon/socks5_socket.rb
|
|
248
|
+
- lib/excon/socks5_ssl_socket.rb
|
|
246
249
|
- lib/excon/ssl_socket.rb
|
|
247
250
|
- lib/excon/test/plugin/server/exec.rb
|
|
248
251
|
- lib/excon/test/plugin/server/puma.rb
|
|
@@ -278,7 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
278
281
|
- !ruby/object:Gem::Version
|
|
279
282
|
version: '0'
|
|
280
283
|
requirements: []
|
|
281
|
-
rubygems_version:
|
|
284
|
+
rubygems_version: 4.0.3
|
|
282
285
|
specification_version: 4
|
|
283
286
|
summary: speed, persistence, http(s)
|
|
284
287
|
test_files: []
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
module Excon
|
|
3
|
-
module Middleware
|
|
4
|
-
class CaptureCookies < Excon::Middleware::Base
|
|
5
|
-
|
|
6
|
-
def extract_cookies_from_set_cookie(set_cookie)
|
|
7
|
-
set_cookie.split(',').map { |full| full.split(';').first.strip }.join('; ')
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def get_header(datum, header)
|
|
11
|
-
_, header_value = datum[:response][:headers].detect do |key, _|
|
|
12
|
-
key.casecmp?(header)
|
|
13
|
-
end
|
|
14
|
-
header_value
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def response_call(datum)
|
|
18
|
-
cookie = get_header(datum, 'Set-Cookie')
|
|
19
|
-
if cookie
|
|
20
|
-
cookie = extract_cookies_from_set_cookie(cookie)
|
|
21
|
-
unless datum[:headers].key?("Cookie")
|
|
22
|
-
datum[:headers]["Cookie"] = cookie
|
|
23
|
-
else
|
|
24
|
-
original_cookies = datum[:headers]["Cookie"]
|
|
25
|
-
datum[:headers]["Cookie"] = original_cookies.empty? ? cookie : [original_cookies,cookie].join('; ')
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
super(datum)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|