http 0.7.4 → 0.8.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop.yml +5 -2
- data/CHANGES.md +24 -7
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +24 -22
- data/Guardfile +2 -2
- data/README.md +34 -4
- data/Rakefile +7 -7
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +12 -12
- data/lib/http.rb +11 -10
- data/lib/http/cache.rb +146 -0
- data/lib/http/cache/headers.rb +100 -0
- data/lib/http/cache/null_cache.rb +13 -0
- data/lib/http/chainable.rb +14 -3
- data/lib/http/client.rb +64 -80
- data/lib/http/connection.rb +139 -0
- data/lib/http/content_type.rb +2 -2
- data/lib/http/errors.rb +7 -1
- data/lib/http/headers.rb +21 -8
- data/lib/http/headers/mixin.rb +1 -1
- data/lib/http/mime_type.rb +2 -2
- data/lib/http/mime_type/adapter.rb +2 -2
- data/lib/http/mime_type/json.rb +4 -4
- data/lib/http/options.rb +65 -74
- data/lib/http/redirector.rb +3 -3
- data/lib/http/request.rb +20 -13
- data/lib/http/request/caching.rb +95 -0
- data/lib/http/request/writer.rb +5 -5
- data/lib/http/response.rb +15 -9
- data/lib/http/response/body.rb +21 -8
- data/lib/http/response/caching.rb +142 -0
- data/lib/http/response/io_body.rb +63 -0
- data/lib/http/response/parser.rb +1 -1
- data/lib/http/response/status.rb +4 -12
- data/lib/http/response/status/reasons.rb +53 -53
- data/lib/http/response/string_body.rb +53 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/cache/headers_spec.rb +77 -0
- data/spec/lib/http/cache_spec.rb +182 -0
- data/spec/lib/http/client_spec.rb +123 -95
- data/spec/lib/http/content_type_spec.rb +25 -25
- data/spec/lib/http/headers/mixin_spec.rb +8 -8
- data/spec/lib/http/headers_spec.rb +213 -173
- data/spec/lib/http/options/body_spec.rb +5 -5
- data/spec/lib/http/options/form_spec.rb +3 -3
- data/spec/lib/http/options/headers_spec.rb +7 -7
- data/spec/lib/http/options/json_spec.rb +3 -3
- data/spec/lib/http/options/merge_spec.rb +26 -22
- data/spec/lib/http/options/new_spec.rb +10 -10
- data/spec/lib/http/options/proxy_spec.rb +8 -8
- data/spec/lib/http/options_spec.rb +2 -2
- data/spec/lib/http/redirector_spec.rb +32 -32
- data/spec/lib/http/request/caching_spec.rb +133 -0
- data/spec/lib/http/request/writer_spec.rb +26 -26
- data/spec/lib/http/request_spec.rb +63 -58
- data/spec/lib/http/response/body_spec.rb +13 -13
- data/spec/lib/http/response/caching_spec.rb +201 -0
- data/spec/lib/http/response/io_body_spec.rb +35 -0
- data/spec/lib/http/response/status_spec.rb +25 -25
- data/spec/lib/http/response/string_body_spec.rb +35 -0
- data/spec/lib/http/response_spec.rb +64 -45
- data/spec/lib/http_spec.rb +103 -76
- data/spec/spec_helper.rb +10 -12
- data/spec/support/connection_reuse_shared.rb +100 -0
- data/spec/support/create_certs.rb +12 -12
- data/spec/support/dummy_server.rb +11 -11
- data/spec/support/dummy_server/servlet.rb +43 -31
- data/spec/support/proxy_server.rb +31 -25
- metadata +57 -8
- data/spec/support/example_server.rb +0 -30
- data/spec/support/example_server/servlet.rb +0 -102
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "simplecov"
|
4
|
+
require "coveralls"
|
5
5
|
|
6
6
|
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
7
7
|
SimpleCov::Formatter::HTMLFormatter,
|
@@ -9,22 +9,20 @@ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
|
9
9
|
]
|
10
10
|
|
11
11
|
SimpleCov.start do
|
12
|
-
add_filter
|
12
|
+
add_filter "/spec/"
|
13
13
|
minimum_coverage 80
|
14
14
|
end
|
15
15
|
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require
|
19
|
-
require 'support/proxy_server'
|
20
|
-
require 'support/capture_warning'
|
16
|
+
require "http"
|
17
|
+
require "rspec/its"
|
18
|
+
require "support/capture_warning"
|
21
19
|
|
22
20
|
# Allow testing against a SSL server
|
23
21
|
def certs_dir
|
24
|
-
Pathname.new File.expand_path(
|
22
|
+
Pathname.new File.expand_path("../../tmp/certs", __FILE__)
|
25
23
|
end
|
26
24
|
|
27
|
-
require
|
25
|
+
require "support/create_certs"
|
28
26
|
|
29
27
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
30
28
|
RSpec.configure do |config|
|
@@ -62,7 +60,7 @@ RSpec.configure do |config|
|
|
62
60
|
|
63
61
|
# This setting enables warnings. It's recommended, but in some cases may
|
64
62
|
# be too noisy due to issues in dependencies.
|
65
|
-
config.warnings =
|
63
|
+
config.warnings = 0 == ENV["GUARD_RSPEC"].to_i
|
66
64
|
|
67
65
|
# Many RSpec users commonly either run the entire suite or an individual
|
68
66
|
# file, and it's useful to allow more verbose output when running an
|
@@ -71,7 +69,7 @@ RSpec.configure do |config|
|
|
71
69
|
# Use the documentation formatter for detailed output,
|
72
70
|
# unless a formatter has already been configured
|
73
71
|
# (e.g. via a command-line flag).
|
74
|
-
config.default_formatter =
|
72
|
+
config.default_formatter = "doc"
|
75
73
|
end
|
76
74
|
|
77
75
|
# Print the 10 slowest examples and example groups at the
|
@@ -0,0 +1,100 @@
|
|
1
|
+
RSpec.shared_context "handles shared connections" do
|
2
|
+
describe "connection reuse" do
|
3
|
+
let(:sockets_used) do
|
4
|
+
[
|
5
|
+
client.get("#{server.endpoint}/socket/1").body.to_s,
|
6
|
+
client.get("#{server.endpoint}/socket/2").body.to_s
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
context "when enabled" do
|
11
|
+
let(:reuse_conn) { server.endpoint }
|
12
|
+
|
13
|
+
context "without a host" do
|
14
|
+
it "infers host from persistent config" do
|
15
|
+
expect(client.get("/").body.to_s).to eq("<!doctype html>")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "re-uses the socket" do
|
20
|
+
expect(sockets_used).to_not include("")
|
21
|
+
expect(sockets_used.uniq.length).to eq(1)
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when trying to read a stale body" do
|
25
|
+
it "errors" do
|
26
|
+
first_res = client.get(server.endpoint)
|
27
|
+
second_res = client.get(server.endpoint)
|
28
|
+
|
29
|
+
# This should work, because it's the last request we made
|
30
|
+
expect(second_res.body.to_s).to eq("<!doctype html>")
|
31
|
+
|
32
|
+
# This should not work because we've not read anything
|
33
|
+
expect { first_res.body.to_s }.to raise_error(HTTP::StateError, /Sequence ID 1 does not match 2/i)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when reading a cached body" do
|
38
|
+
it "succeeds" do
|
39
|
+
first_res = client.get(server.endpoint)
|
40
|
+
first_res.body.to_s
|
41
|
+
|
42
|
+
second_res = client.get(server.endpoint)
|
43
|
+
|
44
|
+
expect(first_res.body.to_s).to eq("<!doctype html>")
|
45
|
+
expect(second_res.body.to_s).to eq("<!doctype html>")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when streaming" do
|
50
|
+
it "errors with a stale response" do
|
51
|
+
first_res = client.get(server.endpoint)
|
52
|
+
second_res = client.get(server.endpoint)
|
53
|
+
|
54
|
+
# This should work, because it's the last request we made
|
55
|
+
expect(second_res.body.to_s).to eq("<!doctype html>")
|
56
|
+
|
57
|
+
# This should not work because we've not read anything
|
58
|
+
expect { first_res.body.each(&:to_s) }.to raise_error(HTTP::StateError, /Sequence ID 1 does not match 2/i)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with a socket issue" do
|
63
|
+
it "transparently reopens" do
|
64
|
+
first_socket = client.get("#{server.endpoint}/socket").body.to_s
|
65
|
+
expect(first_socket).to_not eq("")
|
66
|
+
|
67
|
+
# Kill off the sockets we used
|
68
|
+
# rubocop:disable Style/RescueModifier
|
69
|
+
DummyServer::Servlet.sockets.each do |socket|
|
70
|
+
socket.close rescue nil
|
71
|
+
end
|
72
|
+
DummyServer::Servlet.sockets.clear
|
73
|
+
# rubocop:enable Style/RescueModifier
|
74
|
+
|
75
|
+
# Should error because we tried to use a bad socket
|
76
|
+
expect { client.get("#{server.endpoint}/socket").body.to_s }.to raise_error(IOError)
|
77
|
+
|
78
|
+
# Should succeed since we create a new socket
|
79
|
+
second_socket = client.get("#{server.endpoint}/socket").body.to_s
|
80
|
+
expect(second_socket).to_not eq(first_socket)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "with a change in host" do
|
85
|
+
it "errors" do
|
86
|
+
expect { client.get("https://invalid.com/socket") }.to raise_error(/Persistence is enabled/i)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when disabled" do
|
92
|
+
let(:reuse_conn) { nil }
|
93
|
+
|
94
|
+
it "opens new sockets" do
|
95
|
+
expect(sockets_used).to_not include("")
|
96
|
+
expect(sockets_used.uniq.length).to eq(2)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "fileutils"
|
2
|
+
require "certificate_authority"
|
3
3
|
|
4
4
|
FileUtils.mkdir_p(certs_dir)
|
5
5
|
|
@@ -9,15 +9,15 @@ FileUtils.mkdir_p(certs_dir)
|
|
9
9
|
|
10
10
|
ca = CertificateAuthority::Certificate.new
|
11
11
|
|
12
|
-
ca.subject.common_name =
|
12
|
+
ca.subject.common_name = "honestachmed.com"
|
13
13
|
ca.serial_number.number = 1
|
14
14
|
ca.key_material.generate_key
|
15
15
|
ca.signing_entity = true
|
16
16
|
|
17
|
-
ca.sign!
|
17
|
+
ca.sign! "extensions" => {"keyUsage" => {"usage" => %w(critical keyCertSign)}}
|
18
18
|
|
19
|
-
ca_cert_path = File.join(certs_dir,
|
20
|
-
ca_key_path = File.join(certs_dir,
|
19
|
+
ca_cert_path = File.join(certs_dir, "ca.crt")
|
20
|
+
ca_key_path = File.join(certs_dir, "ca.key")
|
21
21
|
|
22
22
|
File.write ca_cert_path, ca.to_pem
|
23
23
|
File.write ca_key_path, ca.key_material.private_key.to_pem
|
@@ -27,14 +27,14 @@ File.write ca_key_path, ca.key_material.private_key.to_pem
|
|
27
27
|
#
|
28
28
|
|
29
29
|
server_cert = CertificateAuthority::Certificate.new
|
30
|
-
server_cert.subject.common_name =
|
30
|
+
server_cert.subject.common_name = "127.0.0.1"
|
31
31
|
server_cert.serial_number.number = 1
|
32
32
|
server_cert.key_material.generate_key
|
33
33
|
server_cert.parent = ca
|
34
34
|
server_cert.sign!
|
35
35
|
|
36
|
-
server_cert_path = File.join(certs_dir,
|
37
|
-
server_key_path = File.join(certs_dir,
|
36
|
+
server_cert_path = File.join(certs_dir, "server.crt")
|
37
|
+
server_key_path = File.join(certs_dir, "server.key")
|
38
38
|
|
39
39
|
File.write server_cert_path, server_cert.to_pem
|
40
40
|
File.write server_key_path, server_cert.key_material.private_key.to_pem
|
@@ -44,14 +44,14 @@ File.write server_key_path, server_cert.key_material.private_key.to_pem
|
|
44
44
|
#
|
45
45
|
|
46
46
|
client_cert = CertificateAuthority::Certificate.new
|
47
|
-
client_cert.subject.common_name =
|
47
|
+
client_cert.subject.common_name = "127.0.0.1"
|
48
48
|
client_cert.serial_number.number = 1
|
49
49
|
client_cert.key_material.generate_key
|
50
50
|
client_cert.parent = ca
|
51
51
|
client_cert.sign!
|
52
52
|
|
53
|
-
client_cert_path = File.join(certs_dir,
|
54
|
-
client_key_path = File.join(certs_dir,
|
53
|
+
client_cert_path = File.join(certs_dir, "client.crt")
|
54
|
+
client_key_path = File.join(certs_dir, "client.key")
|
55
55
|
|
56
56
|
File.write client_cert_path, client_cert.to_pem
|
57
57
|
File.write client_key_path, client_cert.key_material.private_key.to_pem
|
@@ -1,16 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "webrick"
|
2
|
+
require "webrick/ssl"
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
4
|
+
require "support/black_hole"
|
5
|
+
require "support/dummy_server/servlet"
|
6
|
+
require "support/servers/config"
|
7
|
+
require "support/servers/runner"
|
8
8
|
|
9
9
|
class DummyServer < WEBrick::HTTPServer
|
10
10
|
include ServerConfig
|
11
11
|
|
12
12
|
CONFIG = {
|
13
|
-
:BindAddress =>
|
13
|
+
:BindAddress => "127.0.0.1",
|
14
14
|
:Port => 0,
|
15
15
|
:AccessLog => BlackHole,
|
16
16
|
:Logger => BlackHole
|
@@ -28,7 +28,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
28
28
|
|
29
29
|
super CONFIG.merge(override_config)
|
30
30
|
|
31
|
-
mount(
|
31
|
+
mount("/", Servlet)
|
32
32
|
end
|
33
33
|
|
34
34
|
def endpoint
|
@@ -40,12 +40,12 @@ class DummyServer < WEBrick::HTTPServer
|
|
40
40
|
context = OpenSSL::SSL::SSLContext.new
|
41
41
|
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
42
42
|
context.key = OpenSSL::PKey::RSA.new(
|
43
|
-
File.read(File.join(certs_dir,
|
43
|
+
File.read(File.join(certs_dir, "server.key"))
|
44
44
|
)
|
45
45
|
context.cert = OpenSSL::X509::Certificate.new(
|
46
|
-
File.read(File.join(certs_dir,
|
46
|
+
File.read(File.join(certs_dir, "server.crt"))
|
47
47
|
)
|
48
|
-
context.ca_file = File.join(certs_dir,
|
48
|
+
context.ca_file = File.join(certs_dir, "ca.crt")
|
49
49
|
context
|
50
50
|
end
|
51
51
|
end
|
@@ -1,8 +1,12 @@
|
|
1
1
|
class DummyServer < WEBrick::HTTPServer
|
2
2
|
class Servlet < WEBrick::HTTPServlet::AbstractServlet
|
3
|
+
def self.sockets
|
4
|
+
@sockets ||= []
|
5
|
+
end
|
6
|
+
|
3
7
|
def not_found(_req, res)
|
4
8
|
res.status = 404
|
5
|
-
res.body =
|
9
|
+
res.body = "Not Found"
|
6
10
|
end
|
7
11
|
|
8
12
|
def self.handlers
|
@@ -23,78 +27,86 @@ class DummyServer < WEBrick::HTTPServer
|
|
23
27
|
RUBY
|
24
28
|
end
|
25
29
|
|
26
|
-
get
|
30
|
+
get "/" do |req, res|
|
27
31
|
res.status = 200
|
28
32
|
|
29
|
-
case req[
|
30
|
-
when
|
31
|
-
res[
|
33
|
+
case req["Accept"]
|
34
|
+
when "application/json"
|
35
|
+
res["Content-Type"] = "application/json"
|
32
36
|
res.body = '{"json": true}'
|
33
37
|
else
|
34
|
-
res[
|
35
|
-
res.body =
|
38
|
+
res["Content-Type"] = "text/html"
|
39
|
+
res.body = "<!doctype html>"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
["", "/1", "/2"].each do |path|
|
44
|
+
get "/socket#{path}" do |req, res|
|
45
|
+
self.class.sockets << req.instance_variable_get(:@socket)
|
46
|
+
res.status = 200
|
47
|
+
res.body = req.instance_variable_get(:@socket).object_id.to_s
|
36
48
|
end
|
37
49
|
end
|
38
50
|
|
39
|
-
get
|
40
|
-
next not_found unless
|
51
|
+
get "/params" do |req, res|
|
52
|
+
next not_found unless "foo=bar" == req.query_string
|
41
53
|
|
42
54
|
res.status = 200
|
43
|
-
res.body =
|
55
|
+
res.body = "Params!"
|
44
56
|
end
|
45
57
|
|
46
|
-
get
|
58
|
+
get "/multiple-params" do |req, res|
|
47
59
|
params = CGI.parse req.query_string
|
48
60
|
|
49
|
-
next not_found unless {
|
61
|
+
next not_found unless {"foo" => ["bar"], "baz" => ["quux"]} == params
|
50
62
|
|
51
63
|
res.status = 200
|
52
|
-
res.body =
|
64
|
+
res.body = "More Params!"
|
53
65
|
end
|
54
66
|
|
55
|
-
get
|
67
|
+
get "/proxy" do |_req, res|
|
56
68
|
res.status = 200
|
57
|
-
res.body =
|
69
|
+
res.body = "Proxy!"
|
58
70
|
end
|
59
71
|
|
60
|
-
get
|
72
|
+
get "/not-found" do |_req, res|
|
61
73
|
res.status = 404
|
62
|
-
res.body =
|
74
|
+
res.body = "not found"
|
63
75
|
end
|
64
76
|
|
65
|
-
get
|
77
|
+
get "/redirect-301" do |_req, res|
|
66
78
|
res.status = 301
|
67
|
-
res[
|
79
|
+
res["Location"] = "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}/"
|
68
80
|
end
|
69
81
|
|
70
|
-
get
|
82
|
+
get "/redirect-302" do |_req, res|
|
71
83
|
res.status = 302
|
72
|
-
res[
|
84
|
+
res["Location"] = "http://#{@server.config[:BindAddress]}:#{@server.config[:Port]}/"
|
73
85
|
end
|
74
86
|
|
75
|
-
post
|
76
|
-
if
|
87
|
+
post "/form" do |req, res|
|
88
|
+
if "testing-form" == req.query["example"]
|
77
89
|
res.status = 200
|
78
|
-
res.body =
|
90
|
+
res.body = "passed :)"
|
79
91
|
else
|
80
92
|
res.status = 400
|
81
|
-
res.body =
|
93
|
+
res.body = "invalid! >:E"
|
82
94
|
end
|
83
95
|
end
|
84
96
|
|
85
|
-
post
|
86
|
-
if
|
97
|
+
post "/body" do |req, res|
|
98
|
+
if "testing-body" == req.body
|
87
99
|
res.status = 200
|
88
|
-
res.body =
|
100
|
+
res.body = "passed :)"
|
89
101
|
else
|
90
102
|
res.status = 400
|
91
|
-
res.body =
|
103
|
+
res.body = "invalid! >:E"
|
92
104
|
end
|
93
105
|
end
|
94
106
|
|
95
|
-
head
|
107
|
+
head "/" do |_req, res|
|
96
108
|
res.status = 200
|
97
|
-
res[
|
109
|
+
res["Content-Type"] = "text/html"
|
98
110
|
end
|
99
111
|
end
|
100
112
|
end
|
@@ -1,31 +1,37 @@
|
|
1
|
-
require
|
1
|
+
require "webrick/httpproxy"
|
2
2
|
|
3
|
-
|
3
|
+
require "support/black_hole"
|
4
|
+
require "support/servers/config"
|
5
|
+
require "support/servers/runner"
|
4
6
|
|
5
|
-
ProxyServer
|
6
|
-
|
7
|
-
:AccessLog => [],
|
8
|
-
:RequestCallback => handler
|
9
|
-
)
|
7
|
+
class ProxyServer < WEBrick::HTTPProxyServer
|
8
|
+
include ServerConfig
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
trap('INT') do
|
23
|
-
ProxyServer.shutdown
|
24
|
-
exit
|
10
|
+
CONFIG = {
|
11
|
+
:BindAddress => "127.0.0.1",
|
12
|
+
:Port => 0,
|
13
|
+
:AccessLog => BlackHole,
|
14
|
+
:Logger => BlackHole,
|
15
|
+
:RequestCallback => proc { |_, res| res["X-PROXIED"] = true }
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
super CONFIG
|
20
|
+
end
|
25
21
|
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
class AuthProxyServer < WEBrick::HTTPProxyServer
|
24
|
+
include ServerConfig
|
25
|
+
|
26
|
+
AUTHENTICATOR = proc do |req, res|
|
27
|
+
WEBrick::HTTPAuth.proxy_basic_auth(req, res, "proxy") do |user, pass|
|
28
|
+
user == "username" && pass == "password"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
CONFIG = ProxyServer::CONFIG.merge :ProxyAuthProc => AUTHENTICATOR
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
super CONFIG
|
36
|
+
end
|
31
37
|
end
|