excon 0.16.10 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of excon might be problematic. Click here for more details.
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/excon.gemspec +7 -3
- data/lib/excon.rb +4 -1
- data/lib/excon/connection.rb +218 -205
- data/lib/excon/constants.rb +2 -2
- data/lib/excon/errors.rb +7 -0
- data/lib/excon/middlewares/expects.rb +19 -0
- data/lib/excon/middlewares/instrumentor.rb +26 -0
- data/lib/excon/response.rb +37 -25
- data/lib/excon/socket.rb +37 -20
- data/lib/excon/ssl_socket.rb +20 -25
- data/lib/excon/standard_instrumentor.rb +1 -1
- data/tests/authorization_header_tests.rb +1 -1
- data/tests/{instrumentation_tests.rb → middlewares/instrumentation_tests.rb} +1 -1
- data/tests/proxy_tests.rb +46 -46
- data/tests/rackups/request_headers.ru +11 -0
- data/tests/request_headers_tests.rb +21 -0
- data/tests/test_helper.rb +4 -0
- metadata +9 -5
data/lib/excon/constants.rb
CHANGED
@@ -21,7 +21,7 @@ module Excon
|
|
21
21
|
|
22
22
|
HTTP_1_1 = " HTTP/1.1\r\n"
|
23
23
|
|
24
|
-
HTTP_VERBS = %w{connect delete get head options post put trace}
|
24
|
+
HTTP_VERBS = %w{connect delete get head options post put trace patch}
|
25
25
|
|
26
26
|
HTTPS = 'https'
|
27
27
|
|
@@ -29,7 +29,7 @@ module Excon
|
|
29
29
|
|
30
30
|
REDACTED = 'REDACTED'
|
31
31
|
|
32
|
-
VERSION = '0.
|
32
|
+
VERSION = '0.17.0'
|
33
33
|
|
34
34
|
unless ::IO.const_defined?(:WaitReadable)
|
35
35
|
class ::IO
|
data/lib/excon/errors.rb
CHANGED
@@ -121,6 +121,13 @@ module Excon
|
|
121
121
|
504 => [Excon::Errors::GatewayTimeout, 'Gateway Timeout']
|
122
122
|
}
|
123
123
|
error, message = @errors[response.status] || [Excon::Errors::HTTPStatusError, 'Unknown']
|
124
|
+
|
125
|
+
# scrub authorization
|
126
|
+
if request[:headers].has_key?('Authorization')
|
127
|
+
request = request.dup
|
128
|
+
request[:headers] = request[:headers].dup
|
129
|
+
request[:headers]['Authorization'] = REDACTED
|
130
|
+
end
|
124
131
|
error.new("Expected(#{request[:expects].inspect}) <=> Actual(#{response.status} #{message})\n request => #{request.inspect}\n response => #{response.inspect}", request, response)
|
125
132
|
end
|
126
133
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Excon
|
2
|
+
module Middleware
|
3
|
+
class Expects
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(datum)
|
9
|
+
response_datum = @app.call(datum)
|
10
|
+
|
11
|
+
if datum.has_key?(:expects) && ![*datum[:expects]].include?(response_datum[:status])
|
12
|
+
raise(Excon::Errors.status_error(datum, Excon::Response.new(response_datum)))
|
13
|
+
else
|
14
|
+
response_datum
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Excon
|
2
|
+
module Middleware
|
3
|
+
class Instrumentor
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(datum)
|
9
|
+
if datum.has_key?(:instrumentor)
|
10
|
+
if datum[:retries_remaining] < datum[:retry_limit]
|
11
|
+
event_name = "#{datum[:instrumentor_name]}.retry"
|
12
|
+
else
|
13
|
+
event_name = "#{datum[:instrumentor_name]}.request"
|
14
|
+
end
|
15
|
+
response_datum = datum[:instrumentor].instrument(event_name, datum) do
|
16
|
+
@app.call(datum)
|
17
|
+
end
|
18
|
+
datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.response", response_datum)
|
19
|
+
response_datum
|
20
|
+
else
|
21
|
+
@app.call(datum)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/excon/response.rb
CHANGED
@@ -1,29 +1,41 @@
|
|
1
1
|
module Excon
|
2
2
|
class Response
|
3
3
|
|
4
|
-
attr_accessor :body, :headers, :status
|
4
|
+
attr_accessor :body, :headers, :status, :remote_ip
|
5
5
|
|
6
|
-
def
|
6
|
+
def data
|
7
7
|
{
|
8
|
-
:body
|
9
|
-
:headers
|
10
|
-
:status
|
8
|
+
:body => body,
|
9
|
+
:headers => headers,
|
10
|
+
:status => status,
|
11
|
+
:remote_ip => remote_ip
|
11
12
|
}
|
12
13
|
end
|
13
14
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
@status = attrs[:status]
|
15
|
+
def params
|
16
|
+
$stderr.puts("Excon::Response#params is deprecated use Excon::Response#data instead (#{caller.first})")
|
17
|
+
data
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
|
20
|
+
def initialize(params={})
|
21
|
+
@body = params[:body] || ''
|
22
|
+
@headers = params[:headers] || {}
|
23
|
+
@status = params[:status]
|
24
|
+
@remote_ip = params[:remote_ip]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.parse(socket, datum={})
|
28
|
+
response_datum = {
|
29
|
+
:body => '',
|
30
|
+
:headers => {},
|
31
|
+
:status => socket.read(12)[9, 11].to_i,
|
32
|
+
:remote_ip => socket.remote_ip
|
33
|
+
}
|
22
34
|
socket.readline # read the rest of the status line and CRLF
|
23
35
|
|
24
36
|
until ((data = socket.readline).chop!).empty?
|
25
37
|
key, value = data.split(/:\s*/, 2)
|
26
|
-
|
38
|
+
response_datum[:headers][key] = ([*response_datum[:headers][key]] << value).compact.join(', ')
|
27
39
|
if key.casecmp('Content-Length') == 0
|
28
40
|
content_length = value.to_i
|
29
41
|
elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0)
|
@@ -31,47 +43,47 @@ module Excon
|
|
31
43
|
end
|
32
44
|
end
|
33
45
|
|
34
|
-
unless (['HEAD', 'CONNECT'].include?(
|
46
|
+
unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(response_datum[:status])
|
35
47
|
|
36
48
|
# check to see if expects was set and matched
|
37
|
-
expected_status = !
|
49
|
+
expected_status = !datum.has_key?(:expects) || [*datum[:expects]].include?(response_datum[:status])
|
38
50
|
|
39
51
|
# if expects matched and there is a block, use it
|
40
|
-
if expected_status &&
|
52
|
+
if expected_status && datum.has_key?(:response_block)
|
41
53
|
if transfer_encoding_chunked
|
42
54
|
# 2 == "/r/n".length
|
43
55
|
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
44
|
-
|
56
|
+
datum[:response_block].call(socket.read(chunk_size + 2).chop!, nil, nil)
|
45
57
|
end
|
46
58
|
socket.read(2)
|
47
59
|
elsif remaining = content_length
|
48
60
|
while remaining > 0
|
49
|
-
|
50
|
-
remaining -=
|
61
|
+
datum[:response_block].call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
|
62
|
+
remaining -= datum[:chunk_size]
|
51
63
|
end
|
52
64
|
else
|
53
|
-
while remaining = socket.read(
|
54
|
-
|
65
|
+
while remaining = socket.read(datum[:chunk_size])
|
66
|
+
datum[:response_block].call(remaining, remaining.length, content_length)
|
55
67
|
end
|
56
68
|
end
|
57
69
|
else # no block or unexpected status
|
58
70
|
if transfer_encoding_chunked
|
59
71
|
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
60
|
-
|
72
|
+
response_datum[:body] << socket.read(chunk_size + 2).chop! # 2 == "/r/n".length
|
61
73
|
end
|
62
74
|
socket.read(2) # 2 == "/r/n".length
|
63
75
|
elsif remaining = content_length
|
64
76
|
while remaining > 0
|
65
|
-
|
66
|
-
remaining -=
|
77
|
+
response_datum[:body] << socket.read([datum[:chunk_size], remaining].min)
|
78
|
+
remaining -= datum[:chunk_size]
|
67
79
|
end
|
68
80
|
else
|
69
|
-
|
81
|
+
response_datum[:body] << socket.read
|
70
82
|
end
|
71
83
|
end
|
72
84
|
end
|
73
85
|
|
74
|
-
|
86
|
+
response_datum
|
75
87
|
end
|
76
88
|
|
77
89
|
# Retrieve a specific header value. Header names are treated case-insensitively.
|
data/lib/excon/socket.rb
CHANGED
@@ -3,16 +3,32 @@ module Excon
|
|
3
3
|
|
4
4
|
extend Forwardable
|
5
5
|
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :data
|
7
|
+
|
8
|
+
def params
|
9
|
+
$stderr.puts("Excon::Socket#params is deprecated use Excon::Socket#data instead (#{caller.first})")
|
10
|
+
@data
|
11
|
+
end
|
12
|
+
def params=(new_params)
|
13
|
+
$stderr.puts("Excon::Socket#params= is deprecated use Excon::Socket#data= instead (#{caller.first})")
|
14
|
+
@data = new_params
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :remote_ip
|
7
18
|
|
8
19
|
def_delegators(:@socket, :close, :close)
|
9
20
|
def_delegators(:@socket, :readline, :readline)
|
10
21
|
|
11
|
-
def initialize(
|
12
|
-
@
|
22
|
+
def initialize(data = {})
|
23
|
+
@data = data
|
13
24
|
@read_buffer = ''
|
14
25
|
@eof = false
|
15
26
|
|
27
|
+
@data[:family] ||= ::Socket::Constants::AF_UNSPEC
|
28
|
+
if @data[:proxy]
|
29
|
+
@data[:proxy][:family] ||= ::Socket::Constants::AF_UNSPEC
|
30
|
+
end
|
31
|
+
|
16
32
|
connect
|
17
33
|
end
|
18
34
|
|
@@ -20,24 +36,26 @@ module Excon
|
|
20
36
|
@socket = nil
|
21
37
|
exception = nil
|
22
38
|
|
23
|
-
addrinfo = if @proxy
|
24
|
-
::Socket.getaddrinfo(@proxy[:host], @proxy[:port]
|
39
|
+
addrinfo = if @data[:proxy]
|
40
|
+
::Socket.getaddrinfo(@data[:proxy][:host], @data[:proxy][:port], @data[:proxy][:family], ::Socket::Constants::SOCK_STREAM)
|
25
41
|
else
|
26
|
-
::Socket.getaddrinfo(@
|
42
|
+
::Socket.getaddrinfo(@data[:host], @data[:port], @data[:family], ::Socket::Constants::SOCK_STREAM)
|
27
43
|
end
|
28
44
|
|
29
45
|
addrinfo.each do |_, port, _, ip, a_family, s_type|
|
46
|
+
@remote_ip = ip
|
47
|
+
|
30
48
|
# nonblocking connect
|
31
49
|
begin
|
32
50
|
sockaddr = ::Socket.sockaddr_in(port, ip)
|
33
51
|
|
34
52
|
socket = ::Socket.new(a_family, s_type, 0)
|
35
53
|
|
36
|
-
if @
|
54
|
+
if @data[:nonblock]
|
37
55
|
socket.connect_nonblock(sockaddr)
|
38
56
|
else
|
39
57
|
begin
|
40
|
-
Timeout.timeout(@
|
58
|
+
Timeout.timeout(@data[:connect_timeout]) do
|
41
59
|
socket.connect(sockaddr)
|
42
60
|
end
|
43
61
|
rescue Timeout::Error
|
@@ -48,7 +66,7 @@ module Excon
|
|
48
66
|
@socket = socket
|
49
67
|
break
|
50
68
|
rescue Errno::EINPROGRESS
|
51
|
-
unless IO.select(nil, [socket], nil, @
|
69
|
+
unless IO.select(nil, [socket], nil, @data[:connect_timeout])
|
52
70
|
raise(Excon::Errors::Timeout.new("connect timeout reached"))
|
53
71
|
end
|
54
72
|
begin
|
@@ -76,10 +94,9 @@ module Excon
|
|
76
94
|
end
|
77
95
|
|
78
96
|
def read(max_length=nil)
|
79
|
-
return nil if @eof
|
80
97
|
if @eof
|
81
|
-
|
82
|
-
elsif @
|
98
|
+
return nil
|
99
|
+
elsif @data[:nonblock]
|
83
100
|
begin
|
84
101
|
if max_length
|
85
102
|
until @read_buffer.length >= max_length
|
@@ -87,12 +104,12 @@ module Excon
|
|
87
104
|
end
|
88
105
|
else
|
89
106
|
while true
|
90
|
-
@read_buffer << @socket.read_nonblock(@
|
107
|
+
@read_buffer << @socket.read_nonblock(@data[:chunk_size])
|
91
108
|
end
|
92
109
|
end
|
93
110
|
rescue OpenSSL::SSL::SSLError => error
|
94
111
|
if error.message == 'read would block'
|
95
|
-
if IO.select([@socket], nil, nil, @
|
112
|
+
if IO.select([@socket], nil, nil, @data[:read_timeout])
|
96
113
|
retry
|
97
114
|
else
|
98
115
|
raise(Excon::Errors::Timeout.new("read timeout reached"))
|
@@ -101,7 +118,7 @@ module Excon
|
|
101
118
|
raise(error)
|
102
119
|
end
|
103
120
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
|
104
|
-
if IO.select([@socket], nil, nil, @
|
121
|
+
if IO.select([@socket], nil, nil, @data[:read_timeout])
|
105
122
|
retry
|
106
123
|
else
|
107
124
|
raise(Excon::Errors::Timeout.new("read timeout reached"))
|
@@ -117,7 +134,7 @@ module Excon
|
|
117
134
|
end
|
118
135
|
else
|
119
136
|
begin
|
120
|
-
Timeout.timeout(@
|
137
|
+
Timeout.timeout(@data[:read_timeout]) do
|
121
138
|
@socket.read(max_length)
|
122
139
|
end
|
123
140
|
rescue Timeout::Error
|
@@ -127,7 +144,7 @@ module Excon
|
|
127
144
|
end
|
128
145
|
|
129
146
|
def write(data)
|
130
|
-
if @
|
147
|
+
if @data[:nonblock]
|
131
148
|
# We normally return from the return in the else block below, but
|
132
149
|
# we guard that data is still something in case we get weird
|
133
150
|
# values and String#[] returns nil. (This behavior has been observed
|
@@ -139,7 +156,7 @@ module Excon
|
|
139
156
|
written = @socket.write_nonblock(data)
|
140
157
|
rescue OpenSSL::SSL::SSLError => error
|
141
158
|
if error.message == 'write would block'
|
142
|
-
if IO.select(nil, [@socket], nil, @
|
159
|
+
if IO.select(nil, [@socket], nil, @data[:write_timeout])
|
143
160
|
retry
|
144
161
|
else
|
145
162
|
raise(Excon::Errors::Timeout.new("write timeout reached"))
|
@@ -148,7 +165,7 @@ module Excon
|
|
148
165
|
raise(error)
|
149
166
|
end
|
150
167
|
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable
|
151
|
-
if IO.select(nil, [@socket], nil, @
|
168
|
+
if IO.select(nil, [@socket], nil, @data[:write_timeout])
|
152
169
|
retry
|
153
170
|
else
|
154
171
|
raise(Excon::Errors::Timeout.new("write timeout reached"))
|
@@ -172,7 +189,7 @@ module Excon
|
|
172
189
|
end
|
173
190
|
else
|
174
191
|
begin
|
175
|
-
Timeout.timeout(@
|
192
|
+
Timeout.timeout(@data[:write_timeout]) do
|
176
193
|
@socket.write(data)
|
177
194
|
end
|
178
195
|
rescue Timeout::Error
|
data/lib/excon/ssl_socket.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Excon
|
2
2
|
class SSLSocket < Socket
|
3
3
|
|
4
|
-
def initialize(
|
5
|
-
@
|
4
|
+
def initialize(data = {})
|
5
|
+
@data = data
|
6
6
|
check_nonblock_support
|
7
7
|
|
8
8
|
super
|
@@ -10,36 +10,31 @@ module Excon
|
|
10
10
|
# create ssl context
|
11
11
|
ssl_context = OpenSSL::SSL::SSLContext.new
|
12
12
|
|
13
|
-
if
|
13
|
+
if @data[:ssl_verify_peer]
|
14
14
|
# turn verification on
|
15
15
|
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
16
16
|
|
17
|
-
if
|
18
|
-
ssl_context.ca_path =
|
19
|
-
elsif
|
20
|
-
ssl_context.ca_file =
|
21
|
-
else
|
22
|
-
# use default cert store
|
23
|
-
store = OpenSSL::X509::Store.new
|
24
|
-
store.set_default_paths
|
25
|
-
ssl_context.cert_store = store
|
17
|
+
if @data[:ssl_ca_path]
|
18
|
+
ssl_context.ca_path = @data[:ssl_ca_path]
|
19
|
+
elsif @data[:ssl_ca_file]
|
20
|
+
ssl_context.ca_file = @data[:ssl_ca_file]
|
26
21
|
end
|
27
22
|
else
|
28
23
|
# turn verification off
|
29
24
|
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
30
25
|
end
|
31
26
|
|
32
|
-
if @
|
33
|
-
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@
|
34
|
-
ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@
|
27
|
+
if @data.has_key?(:client_cert) && @data.has_key?(:client_key)
|
28
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@data[:client_cert]))
|
29
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@data[:client_key]))
|
35
30
|
end
|
36
31
|
|
37
|
-
if @proxy
|
38
|
-
request = 'CONNECT ' << @
|
39
|
-
request << 'Host: ' << @
|
32
|
+
if @data[:proxy]
|
33
|
+
request = 'CONNECT ' << @data[:host_port] << Excon::HTTP_1_1
|
34
|
+
request << 'Host: ' << @data[:host_port] << Excon::CR_NL
|
40
35
|
|
41
|
-
if @proxy[:password] || @proxy[:user]
|
42
|
-
auth = ['' << @proxy[:user].to_s << ':' << @proxy[:password].to_s].pack('m').delete(Excon::CR_NL)
|
36
|
+
if @data[:proxy][:password] || @data[:proxy][:user]
|
37
|
+
auth = ['' << @data[:proxy][:user].to_s << ':' << @data[:proxy][:password].to_s].pack('m').delete(Excon::CR_NL)
|
43
38
|
request << "Proxy-Authorization: Basic " << auth << Excon::CR_NL
|
44
39
|
end
|
45
40
|
|
@@ -61,12 +56,12 @@ module Excon
|
|
61
56
|
|
62
57
|
# Server Name Indication (SNI) RFC 3546
|
63
58
|
if @socket.respond_to?(:hostname=)
|
64
|
-
@socket.hostname = @
|
59
|
+
@socket.hostname = @data[:host]
|
65
60
|
end
|
66
61
|
|
67
62
|
# verify connection
|
68
|
-
if
|
69
|
-
@socket.post_connection_check(@
|
63
|
+
if @data[:ssl_verify_peer]
|
64
|
+
@socket.post_connection_check(@data[:host])
|
70
65
|
end
|
71
66
|
|
72
67
|
@socket
|
@@ -91,9 +86,9 @@ module Excon
|
|
91
86
|
|
92
87
|
def check_nonblock_support
|
93
88
|
# backwards compatability for things lacking nonblock
|
94
|
-
if !DEFAULT_NONBLOCK &&
|
89
|
+
if !DEFAULT_NONBLOCK && @data[:nonblock]
|
95
90
|
$stderr.puts("Excon nonblock is not supported by your OpenSSL::SSL::SSLSocket")
|
96
|
-
|
91
|
+
@data[:nonblock] = false
|
97
92
|
end
|
98
93
|
end
|
99
94
|
|