excon 0.45.4 → 0.46.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.
- checksums.yaml +4 -4
- data/CONTRIBUTORS.md +3 -1
- data/Gemfile.lock +11 -0
- data/README.md +14 -3
- data/changelog.txt +12 -0
- data/data/cacert.pem +9 -3988
- data/excon.gemspec +6 -2
- data/lib/excon.rb +1 -0
- data/lib/excon/connection.rb +1 -1
- data/lib/excon/constants.rb +1 -1
- data/lib/excon/errors.rb +1 -1
- data/lib/excon/middlewares/capture_cookies.rb +31 -0
- data/lib/excon/middlewares/decompress.rb +4 -3
- data/lib/excon/middlewares/redirect_follower.rb +10 -3
- data/lib/excon/response.rb +33 -19
- data/lib/excon/utils.rb +3 -2
- data/tests/basic_tests.rb +15 -0
- data/tests/middlewares/capture_cookies_tests.rb +34 -0
- data/tests/middlewares/decompress_tests.rb +5 -0
- data/tests/query_string_tests.rb +22 -4
- data/tests/rackups/basic.rb +6 -0
- data/tests/rackups/redirecting_with_cookie.ru +40 -0
- data/tests/servers/good.rb +8 -3
- metadata +21 -3
data/excon.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'excon'
|
16
|
-
s.version = '0.
|
17
|
-
s.date = '
|
16
|
+
s.version = '0.46.0'
|
17
|
+
s.date = '2016-02-26'
|
18
18
|
s.rubyforge_project = 'excon'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -62,6 +62,7 @@ Gem::Specification.new do |s|
|
|
62
62
|
s.add_development_dependency('rdoc')
|
63
63
|
s.add_development_dependency('shindo')
|
64
64
|
s.add_development_dependency('sinatra')
|
65
|
+
s.add_development_dependency('sinatra-contrib')
|
65
66
|
s.add_development_dependency('json', '>= 1.8.2')
|
66
67
|
|
67
68
|
## Leave this section as-is. It will be automatically generated from the
|
@@ -104,6 +105,7 @@ Gem::Specification.new do |s|
|
|
104
105
|
lib/excon/extensions/uri.rb
|
105
106
|
lib/excon/headers.rb
|
106
107
|
lib/excon/middlewares/base.rb
|
108
|
+
lib/excon/middlewares/capture_cookies.rb
|
107
109
|
lib/excon/middlewares/decompress.rb
|
108
110
|
lib/excon/middlewares/escape_path.rb
|
109
111
|
lib/excon/middlewares/expects.rb
|
@@ -131,6 +133,7 @@ Gem::Specification.new do |s|
|
|
131
133
|
tests/errors_tests.rb
|
132
134
|
tests/header_tests.rb
|
133
135
|
tests/middlewares/canned_response_tests.rb
|
136
|
+
tests/middlewares/capture_cookies_tests.rb
|
134
137
|
tests/middlewares/decompress_tests.rb
|
135
138
|
tests/middlewares/escape_path_tests.rb
|
136
139
|
tests/middlewares/idempotent_tests.rb
|
@@ -147,6 +150,7 @@ Gem::Specification.new do |s|
|
|
147
150
|
tests/rackups/proxy.ru
|
148
151
|
tests/rackups/query_string.ru
|
149
152
|
tests/rackups/redirecting.ru
|
153
|
+
tests/rackups/redirecting_with_cookie.ru
|
150
154
|
tests/rackups/request_headers.ru
|
151
155
|
tests/rackups/request_methods.ru
|
152
156
|
tests/rackups/response_header.ru
|
data/lib/excon.rb
CHANGED
@@ -30,6 +30,7 @@ require 'excon/response'
|
|
30
30
|
require 'excon/middlewares/decompress'
|
31
31
|
require 'excon/middlewares/escape_path'
|
32
32
|
require 'excon/middlewares/redirect_follower'
|
33
|
+
require 'excon/middlewares/capture_cookies'
|
33
34
|
require 'excon/pretty_printer'
|
34
35
|
require 'excon/socket'
|
35
36
|
require 'excon/ssl_socket'
|
data/lib/excon/connection.rb
CHANGED
@@ -207,7 +207,7 @@ module Excon
|
|
207
207
|
datum[:headers] = @data[:headers].merge(datum[:headers] || {})
|
208
208
|
|
209
209
|
if datum[:scheme] == UNIX
|
210
|
-
datum[:headers]['Host']
|
210
|
+
datum[:headers]['Host'] = ''
|
211
211
|
else
|
212
212
|
datum[:headers]['Host'] ||= '' << datum[:host] << port_string(datum)
|
213
213
|
end
|
data/lib/excon/constants.rb
CHANGED
data/lib/excon/errors.rb
CHANGED
@@ -8,7 +8,7 @@ module Excon
|
|
8
8
|
class SocketError < Error
|
9
9
|
attr_reader :socket_error
|
10
10
|
|
11
|
-
def initialize(socket_error=Excon::Error.new)
|
11
|
+
def initialize(socket_error=Excon::Errors::Error.new)
|
12
12
|
if socket_error.message =~ /certificate verify failed/
|
13
13
|
super("Unable to verify certificate, please set `Excon.defaults[:ssl_ca_path] = path_to_certs`, `ENV['SSL_CERT_DIR'] = path_to_certs`, `Excon.defaults[:ssl_ca_file] = path_to_file`, `ENV['SSL_CERT_FILE'] = path_to_file`, `Excon.defaults[:ssl_verify_callback] = callback` (see OpenSSL::SSL::SSLContext#verify_callback), or `Excon.defaults[:ssl_verify_peer] = false` (less secure).")
|
14
14
|
else
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Excon
|
2
|
+
module Middleware
|
3
|
+
class CaptureCookies < Excon::Middleware::Base
|
4
|
+
|
5
|
+
def extract_cookies_from_set_cookie(set_cookie)
|
6
|
+
set_cookie.split(',').map { |full| full.split(';').first.strip }.join('; ')
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_header(datum, header)
|
10
|
+
_, header_value = datum[:response][:headers].detect do |key, value|
|
11
|
+
key.casecmp(header) == 0
|
12
|
+
end
|
13
|
+
header_value
|
14
|
+
end
|
15
|
+
|
16
|
+
def response_call(datum)
|
17
|
+
cookie = get_header(datum, 'Set-Cookie')
|
18
|
+
if cookie
|
19
|
+
cookie = extract_cookies_from_set_cookie(cookie)
|
20
|
+
unless datum[:headers].key?("Cookie")
|
21
|
+
datum[:headers]["Cookie"] = cookie
|
22
|
+
else
|
23
|
+
original_cookies = datum[:headers]["Cookie"]
|
24
|
+
datum[:headers]["Cookie"] = original_cookies.empty? ? cookie : [original_cookies,cookie].join('; ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
super(datum)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -12,16 +12,17 @@ module Excon
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def response_call(datum)
|
15
|
-
|
15
|
+
body = datum[:response][:body]
|
16
|
+
unless datum.has_key?(:response_block) || body.nil? || body.empty?
|
16
17
|
if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 }
|
17
18
|
encodings = Utils.split_header_value(datum[:response][:headers][key])
|
18
19
|
if encoding = encodings.last
|
19
20
|
if encoding.casecmp('deflate') == 0
|
20
21
|
# assume inflate omits header
|
21
|
-
datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(
|
22
|
+
datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(body)
|
22
23
|
encodings.pop
|
23
24
|
elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0
|
24
|
-
datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(
|
25
|
+
datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(body)).read
|
25
26
|
encodings.pop
|
26
27
|
end
|
27
28
|
datum[:response][:headers][key] = encodings.join(', ')
|
@@ -1,14 +1,21 @@
|
|
1
1
|
module Excon
|
2
2
|
module Middleware
|
3
3
|
class RedirectFollower < Excon::Middleware::Base
|
4
|
+
|
5
|
+
def get_header(datum, header)
|
6
|
+
_, header_value = datum[:response][:headers].detect do |key, value|
|
7
|
+
key.casecmp(header) == 0
|
8
|
+
end
|
9
|
+
header_value
|
10
|
+
end
|
11
|
+
|
4
12
|
def response_call(datum)
|
5
13
|
if datum.has_key?(:response)
|
6
14
|
case datum[:response][:status]
|
7
15
|
when 301, 302, 303, 307, 308
|
8
16
|
uri_parser = datum[:uri_parser] || Excon.defaults[:uri_parser]
|
9
|
-
|
10
|
-
|
11
|
-
end
|
17
|
+
|
18
|
+
location = get_header(datum, 'Location')
|
12
19
|
uri = uri_parser.parse(location)
|
13
20
|
|
14
21
|
# delete old/redirect response
|
data/lib/excon/response.rb
CHANGED
@@ -16,17 +16,20 @@ module Excon
|
|
16
16
|
def headers
|
17
17
|
@data[:headers]
|
18
18
|
end
|
19
|
-
def
|
20
|
-
@data[:
|
19
|
+
def host
|
20
|
+
@data[:host]
|
21
21
|
end
|
22
|
-
def
|
23
|
-
@data[:
|
22
|
+
def local_address
|
23
|
+
@data[:local_address]
|
24
24
|
end
|
25
|
-
def
|
26
|
-
@data[:
|
25
|
+
def local_port
|
26
|
+
@data[:local_port]
|
27
27
|
end
|
28
|
-
def
|
29
|
-
@data[:
|
28
|
+
def path
|
29
|
+
@data[:path]
|
30
|
+
end
|
31
|
+
def port
|
32
|
+
@data[:port]
|
30
33
|
end
|
31
34
|
def reason_phrase=(new_reason_phrase)
|
32
35
|
@data[:reason_phrase] = new_reason_phrase
|
@@ -40,11 +43,17 @@ module Excon
|
|
40
43
|
def remote_ip
|
41
44
|
@data[:remote_ip]
|
42
45
|
end
|
43
|
-
def
|
44
|
-
@data[:
|
46
|
+
def status=(new_status)
|
47
|
+
@data[:status] = new_status
|
45
48
|
end
|
46
|
-
def
|
47
|
-
@data[:
|
49
|
+
def status
|
50
|
+
@data[:status]
|
51
|
+
end
|
52
|
+
def status_line
|
53
|
+
@data[:status_line]
|
54
|
+
end
|
55
|
+
def status_line=(new_status_line)
|
56
|
+
@data[:status_line] = new_status_line
|
48
57
|
end
|
49
58
|
|
50
59
|
def self.parse(socket, datum)
|
@@ -57,7 +66,10 @@ module Excon
|
|
57
66
|
|
58
67
|
datum[:response] = {
|
59
68
|
:body => '',
|
69
|
+
:host => datum[:host],
|
60
70
|
:headers => Excon::Headers.new,
|
71
|
+
:path => datum[:path],
|
72
|
+
:port => datum[:port],
|
61
73
|
:status => status,
|
62
74
|
:status_line => line,
|
63
75
|
:reason_phrase => reason_phrase
|
@@ -100,25 +112,27 @@ module Excon
|
|
100
112
|
if response_block
|
101
113
|
while (chunk_size = socket.readline.chomp!.to_i(16)) > 0
|
102
114
|
while chunk_size > 0
|
103
|
-
chunk = socket.read(chunk_size)
|
115
|
+
chunk = socket.read(chunk_size) || raise(EOFError)
|
104
116
|
chunk_size -= chunk.bytesize
|
105
117
|
response_block.call(chunk, nil, nil)
|
106
118
|
end
|
107
119
|
new_line_size = 2 # 2 == "\r\n".length
|
108
120
|
while new_line_size > 0
|
109
|
-
|
121
|
+
chunk = socket.read(new_line_size) || raise(EOFError)
|
122
|
+
new_line_size -= chunk.length
|
110
123
|
end
|
111
124
|
end
|
112
125
|
else
|
113
126
|
while (chunk_size = socket.readline.chomp!.to_i(16)) > 0
|
114
127
|
while chunk_size > 0
|
115
|
-
chunk = socket.read(chunk_size)
|
128
|
+
chunk = socket.read(chunk_size) || raise(EOFError)
|
116
129
|
chunk_size -= chunk.bytesize
|
117
130
|
datum[:response][:body] << chunk
|
118
131
|
end
|
119
132
|
new_line_size = 2 # 2 == "\r\n".length
|
120
133
|
while new_line_size > 0
|
121
|
-
|
134
|
+
chunk = socket.read(new_line_size) || raise(EOFError)
|
135
|
+
new_line_size -= chunk.length
|
122
136
|
end
|
123
137
|
end
|
124
138
|
end
|
@@ -131,13 +145,13 @@ module Excon
|
|
131
145
|
if remaining = content_length
|
132
146
|
if response_block
|
133
147
|
while remaining > 0
|
134
|
-
chunk = socket.read([datum[:chunk_size], remaining].min)
|
148
|
+
chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
|
135
149
|
response_block.call(chunk, [remaining - chunk.bytesize, 0].max, content_length)
|
136
150
|
remaining -= chunk.bytesize
|
137
151
|
end
|
138
152
|
else
|
139
153
|
while remaining > 0
|
140
|
-
chunk = socket.read([datum[:chunk_size], remaining].min)
|
154
|
+
chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
|
141
155
|
datum[:response][:body] << chunk
|
142
156
|
remaining -= chunk.bytesize
|
143
157
|
end
|
@@ -160,7 +174,7 @@ module Excon
|
|
160
174
|
|
161
175
|
def self.parse_headers(socket, datum)
|
162
176
|
last_key = nil
|
163
|
-
until (data = socket.readline.chomp
|
177
|
+
until (data = socket.readline.chomp).empty?
|
164
178
|
if !data.lstrip!.nil?
|
165
179
|
raise Excon::Errors::ResponseParseError, 'malformed header' unless last_key
|
166
180
|
# append to last_key's last value
|
data/lib/excon/utils.rb
CHANGED
@@ -40,11 +40,12 @@ module Excon
|
|
40
40
|
when Hash
|
41
41
|
str << '?'
|
42
42
|
datum[:query].sort_by {|k,_| k.to_s }.each do |key, values|
|
43
|
+
key = CGI.escape(key.to_s)
|
43
44
|
if values.nil?
|
44
|
-
str << key
|
45
|
+
str << key << '&'
|
45
46
|
else
|
46
47
|
[values].flatten.each do |value|
|
47
|
-
str << key
|
48
|
+
str << key << '=' << CGI.escape(value.to_s) << '&'
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
data/tests/basic_tests.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
1
3
|
Shindo.tests('Excon basics') do
|
2
4
|
with_rackup('basic.ru') do
|
3
5
|
basic_tests
|
@@ -246,6 +248,19 @@ Shindo.tests('Excon basics (Unix socket)') do
|
|
246
248
|
response[:status]
|
247
249
|
end
|
248
250
|
end
|
251
|
+
|
252
|
+
tests('http Host header is empty') do
|
253
|
+
tests('GET /headers').returns("") do
|
254
|
+
connection = Excon::Connection.new({
|
255
|
+
:socket => file_name,
|
256
|
+
:nonblock => false,
|
257
|
+
:scheme => 'unix',
|
258
|
+
:ssl_verify_peer => false
|
259
|
+
})
|
260
|
+
response = connection.request(:method => :get, :path => '/headers')
|
261
|
+
JSON.parse(response.body)['HTTP_HOST']
|
262
|
+
end
|
263
|
+
end
|
249
264
|
end
|
250
265
|
end
|
251
266
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Shindo.tests("Excon redirecting with cookie preserved") do
|
2
|
+
env_init
|
3
|
+
|
4
|
+
with_rackup('redirecting_with_cookie.ru') do
|
5
|
+
tests('second request will send cookies set by the first').returns('ok') do
|
6
|
+
Excon.get(
|
7
|
+
'http://127.0.0.1:9292',
|
8
|
+
:path => '/sets_cookie',
|
9
|
+
:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::CaptureCookies, Excon::Middleware::RedirectFollower]
|
10
|
+
).body
|
11
|
+
end
|
12
|
+
|
13
|
+
tests('second request will send multiple cookies set by the first').returns('ok') do
|
14
|
+
Excon.get(
|
15
|
+
'http://127.0.0.1:9292',
|
16
|
+
:path => '/sets_multi_cookie',
|
17
|
+
:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::CaptureCookies, Excon::Middleware::RedirectFollower]
|
18
|
+
).body
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
with_rackup('redirecting.ru') do
|
23
|
+
tests("runs normally when there are no cookies set").returns('ok') do
|
24
|
+
Excon.post(
|
25
|
+
'http://127.0.0.1:9292',
|
26
|
+
:path => '/first',
|
27
|
+
:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::CaptureCookies, Excon::Middleware::RedirectFollower],
|
28
|
+
:body => "a=Some_content"
|
29
|
+
).body
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
env_restore
|
34
|
+
end
|
@@ -29,6 +29,11 @@ Shindo.tests('Excon Decompress Middleware') do
|
|
29
29
|
tests('removes processed encoding from header').returns('') do
|
30
30
|
resp[:headers]['Content-Encoding']
|
31
31
|
end
|
32
|
+
|
33
|
+
tests('empty response body').returns('') do
|
34
|
+
resp = @connection.request(:body => '')
|
35
|
+
resp[:body]
|
36
|
+
end
|
32
37
|
end
|
33
38
|
|
34
39
|
tests('deflate') do
|
data/tests/query_string_tests.rb
CHANGED
@@ -52,12 +52,12 @@ Shindo.tests('Excon query string variants') do
|
|
52
52
|
response = connection.request(:method => :get, :path => '/query', :query => {'foo[]' => ['bar', 'baz'], :me => 'too'})
|
53
53
|
query_string = response.body[7..-1] # query string sent
|
54
54
|
|
55
|
-
test("query string sent includes 'foo
|
56
|
-
query_string.split('&').include?('foo
|
55
|
+
test("query string sent includes 'foo%5B%5D=bar'") do
|
56
|
+
query_string.split('&').include?('foo%5B%5D=bar')
|
57
57
|
end
|
58
58
|
|
59
|
-
test("query string sent includes 'foo
|
60
|
-
query_string.split('&').include?('foo
|
59
|
+
test("query string sent includes 'foo%5B%5D=baz'") do
|
60
|
+
query_string.split('&').include?('foo%5B%5D=baz')
|
61
61
|
end
|
62
62
|
|
63
63
|
test("query string sent includes 'me=too'") do
|
@@ -65,5 +65,23 @@ Shindo.tests('Excon query string variants') do
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
+
tests(":query => {'foo%=#' => 'bar%=#'}") do
|
69
|
+
response = connection.request(:method => :get, :path => '/query', :query => {'foo%=#' => 'bar%=#'})
|
70
|
+
query_string = response.body[7..-1] # query string sent
|
71
|
+
|
72
|
+
tests("query string sent").returns('foo%25%3D%23=bar%25%3D%23') do
|
73
|
+
query_string
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
tests(":query => {'foo%=#' => nil}") do
|
78
|
+
response = connection.request(:method => :get, :path => '/query', :query => {'foo%=#' => nil})
|
79
|
+
query_string = response.body[7..-1] # query string sent
|
80
|
+
|
81
|
+
tests("query string sent").returns('foo%25%3D%23') do
|
82
|
+
query_string
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
68
86
|
end
|
69
87
|
end
|
data/tests/rackups/basic.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'sinatra'
|
2
|
+
require 'json'
|
2
3
|
require File.join(File.dirname(__FILE__), 'webrick_patch')
|
3
4
|
|
4
5
|
class Basic < Sinatra::Base
|
@@ -10,6 +11,11 @@ class Basic < Sinatra::Base
|
|
10
11
|
'x' * value.to_i
|
11
12
|
end
|
12
13
|
|
14
|
+
get('/headers') do
|
15
|
+
content_type :json
|
16
|
+
request.env.select{|key, _| key.start_with? 'HTTP_'}.to_json
|
17
|
+
end
|
18
|
+
|
13
19
|
post('/body-sink') do
|
14
20
|
request.body.read.size.to_s
|
15
21
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sinatra/cookies'
|
3
|
+
require 'json'
|
4
|
+
require File.join(File.dirname(__FILE__), 'webrick_patch')
|
5
|
+
|
6
|
+
class App < Sinatra::Base
|
7
|
+
helpers Sinatra::Cookies
|
8
|
+
set :environment, :production
|
9
|
+
enable :dump_errors
|
10
|
+
|
11
|
+
get('/sets_cookie') do
|
12
|
+
cookies[:chocolatechip] = "chunky"
|
13
|
+
redirect "/requires_cookie"
|
14
|
+
end
|
15
|
+
|
16
|
+
get('/requires_cookie') do
|
17
|
+
cookie = cookies[:chocolatechip]
|
18
|
+
unless cookie.nil? || cookie != "chunky"
|
19
|
+
"ok"
|
20
|
+
else
|
21
|
+
JSON.pretty_generate(headers)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
get('/sets_multi_cookie') do
|
26
|
+
cookies[:chocolatechip] = "chunky"
|
27
|
+
cookies[:thinmints] = "minty"
|
28
|
+
redirect "/requires_cookie"
|
29
|
+
end
|
30
|
+
|
31
|
+
get('/requires_cookie') do
|
32
|
+
if cookies[:chocolatechip] == "chunky" && cookies[:thinmints] == "minty"
|
33
|
+
"ok"
|
34
|
+
else
|
35
|
+
JSON.pretty_generate(headers)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
run App
|