http 6.0.0 → 6.0.1
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/http.gemspec +2 -2
- data/lib/http/version.rb +1 -1
- metadata +4 -79
- data/CHANGELOG.md +0 -267
- data/CONTRIBUTING.md +0 -26
- data/SECURITY.md +0 -17
- data/UPGRADING.md +0 -491
- data/sig/deps.rbs +0 -122
- data/test/http/base64_test.rb +0 -28
- data/test/http/client_test.rb +0 -739
- data/test/http/connection_test.rb +0 -1533
- data/test/http/content_type_test.rb +0 -190
- data/test/http/errors_test.rb +0 -28
- data/test/http/feature_test.rb +0 -49
- data/test/http/features/auto_deflate_test.rb +0 -317
- data/test/http/features/auto_inflate_test.rb +0 -213
- data/test/http/features/caching_test.rb +0 -942
- data/test/http/features/digest_auth_test.rb +0 -996
- data/test/http/features/instrumentation_test.rb +0 -246
- data/test/http/features/logging_test.rb +0 -654
- data/test/http/features/normalize_uri_test.rb +0 -41
- data/test/http/features/raise_error_test.rb +0 -77
- data/test/http/form_data/composite_io_test.rb +0 -215
- data/test/http/form_data/file_test.rb +0 -255
- data/test/http/form_data/fixtures/the-http-gem.info +0 -1
- data/test/http/form_data/multipart_test.rb +0 -303
- data/test/http/form_data/part_test.rb +0 -90
- data/test/http/form_data/urlencoded_test.rb +0 -164
- data/test/http/form_data_test.rb +0 -232
- data/test/http/headers/normalizer_test.rb +0 -93
- data/test/http/headers_test.rb +0 -888
- data/test/http/mime_type/json_test.rb +0 -39
- data/test/http/mime_type_test.rb +0 -150
- data/test/http/options/base_uri_test.rb +0 -148
- data/test/http/options/body_test.rb +0 -21
- data/test/http/options/features_test.rb +0 -38
- data/test/http/options/form_test.rb +0 -21
- data/test/http/options/headers_test.rb +0 -32
- data/test/http/options/json_test.rb +0 -21
- data/test/http/options/merge_test.rb +0 -78
- data/test/http/options/new_test.rb +0 -37
- data/test/http/options/proxy_test.rb +0 -32
- data/test/http/options_test.rb +0 -575
- data/test/http/redirector_test.rb +0 -639
- data/test/http/request/body_test.rb +0 -318
- data/test/http/request/builder_test.rb +0 -623
- data/test/http/request/writer_test.rb +0 -391
- data/test/http/request_test.rb +0 -1733
- data/test/http/response/body_test.rb +0 -292
- data/test/http/response/parser_test.rb +0 -105
- data/test/http/response/status_test.rb +0 -322
- data/test/http/response_test.rb +0 -502
- data/test/http/retriable/delay_calculator_test.rb +0 -194
- data/test/http/retriable/errors_test.rb +0 -71
- data/test/http/retriable/performer_test.rb +0 -551
- data/test/http/session_test.rb +0 -424
- data/test/http/timeout/global_test.rb +0 -239
- data/test/http/timeout/null_test.rb +0 -218
- data/test/http/timeout/per_operation_test.rb +0 -220
- data/test/http/uri/normalizer_test.rb +0 -89
- data/test/http/uri_test.rb +0 -1140
- data/test/http/version_test.rb +0 -15
- data/test/http_test.rb +0 -818
- data/test/regression_tests.rb +0 -27
- data/test/support/capture_warning.rb +0 -10
- data/test/support/dummy_server/encoding_routes.rb +0 -47
- data/test/support/dummy_server/routes.rb +0 -201
- data/test/support/dummy_server/servlet.rb +0 -81
- data/test/support/dummy_server.rb +0 -200
- data/test/support/fakeio.rb +0 -21
- data/test/support/http_handling_shared/connection_reuse_tests.rb +0 -97
- data/test/support/http_handling_shared/timeout_tests.rb +0 -134
- data/test/support/http_handling_shared.rb +0 -11
- data/test/support/proxy_server.rb +0 -207
- data/test/support/servers/runner.rb +0 -67
- data/test/support/simplecov.rb +0 -28
- data/test/support/ssl_helper.rb +0 -108
- data/test/test_helper.rb +0 -38
data/test/regression_tests.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_helper"
|
|
4
|
-
|
|
5
|
-
class RegressionTest < Minitest::Test
|
|
6
|
-
# #248
|
|
7
|
-
|
|
8
|
-
def test_248_does_not_fail_with_github
|
|
9
|
-
github_uri = "http://github.com/"
|
|
10
|
-
HTTP.get(github_uri).to_s
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def test_248_does_not_fail_with_googleapis
|
|
14
|
-
google_uri = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json"
|
|
15
|
-
HTTP.get(google_uri).to_s
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# #422
|
|
19
|
-
|
|
20
|
-
def test_422_reads_body_when_200_ok_response_contains_upgrade_header
|
|
21
|
-
res = HTTP.get("https://httpbin.org/response-headers?Upgrade=h2,h2c")
|
|
22
|
-
parsed = res.parse(:json)
|
|
23
|
-
|
|
24
|
-
assert_includes parsed, "Upgrade"
|
|
25
|
-
assert_equal "h2,h2c", parsed["Upgrade"]
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class DummyServer
|
|
4
|
-
class Servlet
|
|
5
|
-
post "/encoded-body" do |req, res|
|
|
6
|
-
res.status = 200
|
|
7
|
-
body = request_body(req)
|
|
8
|
-
|
|
9
|
-
res.body = case request_header(req, "Accept-Encoding")
|
|
10
|
-
when "gzip"
|
|
11
|
-
res["Content-Encoding"] = "gzip"
|
|
12
|
-
StringIO.open do |out|
|
|
13
|
-
Zlib::GzipWriter.wrap(out) do |gz|
|
|
14
|
-
gz.write "#{body}-gzipped"
|
|
15
|
-
gz.finish
|
|
16
|
-
out.tap(&:rewind).read
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
when "deflate"
|
|
20
|
-
res["Content-Encoding"] = "deflate"
|
|
21
|
-
Zlib::Deflate.deflate("#{body}-deflated")
|
|
22
|
-
else
|
|
23
|
-
"#{body}-raw"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
post "/no-content-204" do |req, res|
|
|
28
|
-
res.status = 204
|
|
29
|
-
res.body = ""
|
|
30
|
-
|
|
31
|
-
case request_header(req, "Accept-Encoding")
|
|
32
|
-
when "gzip"
|
|
33
|
-
res["Content-Encoding"] = "gzip"
|
|
34
|
-
when "deflate"
|
|
35
|
-
res["Content-Encoding"] = "deflate"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
get "/retry-2" do |_req, res|
|
|
40
|
-
@memo[:attempts] ||= 0
|
|
41
|
-
@memo[:attempts] += 1
|
|
42
|
-
|
|
43
|
-
res.body = "retried #{@memo[:attempts]}x"
|
|
44
|
-
res.status = @memo[:attempts] == 2 ? 200 : 500
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class DummyServer
|
|
4
|
-
class Servlet
|
|
5
|
-
get "/" do |req, res|
|
|
6
|
-
res.status = 200
|
|
7
|
-
|
|
8
|
-
case req["Accept"]
|
|
9
|
-
when "application/json"
|
|
10
|
-
res["Content-Type"] = "application/json"
|
|
11
|
-
res.body = '{"json": true}'
|
|
12
|
-
else
|
|
13
|
-
res["Content-Type"] = "text/html"
|
|
14
|
-
res.body = "<!doctype html>"
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
get "/sleep" do |_req, res|
|
|
19
|
-
sleep 0.02
|
|
20
|
-
|
|
21
|
-
res.status = 200
|
|
22
|
-
res.body = "hello"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
post "/sleep" do |_req, res|
|
|
26
|
-
sleep 0.02
|
|
27
|
-
|
|
28
|
-
res.status = 200
|
|
29
|
-
res.body = "hello"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
["", "/1", "/2"].each do |path|
|
|
33
|
-
get "/socket#{path}" do |req, res|
|
|
34
|
-
socket = req.socket
|
|
35
|
-
self.class.sockets << socket
|
|
36
|
-
res.status = 200
|
|
37
|
-
res.body = socket.object_id.to_s
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
get "/params" do |req, res|
|
|
42
|
-
if "foo=bar" == query_string(req)
|
|
43
|
-
res.status = 200
|
|
44
|
-
res.body = "Params!"
|
|
45
|
-
else
|
|
46
|
-
res.status = 404
|
|
47
|
-
res.body = "#{req.unparsed_uri} not found"
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
get "/multiple-params" do |req, res|
|
|
52
|
-
params = URI.decode_www_form(query_string(req)).group_by(&:first).transform_values { |v| v.map(&:last) }
|
|
53
|
-
|
|
54
|
-
if { "foo" => ["bar"], "baz" => ["quux"] } == params
|
|
55
|
-
res.status = 200
|
|
56
|
-
res.body = "More Params!"
|
|
57
|
-
else
|
|
58
|
-
res.status = 404
|
|
59
|
-
res.body = "#{req.unparsed_uri} not found"
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
get "/proxy" do |_req, res|
|
|
64
|
-
res.status = 200
|
|
65
|
-
res.body = "Proxy!"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
get "/not-found" do |_req, res|
|
|
69
|
-
res.status = 404
|
|
70
|
-
res.body = "not found"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
get "/redirect-301" do |_req, res|
|
|
74
|
-
res.status = 301
|
|
75
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
get "/redirect-302" do |_req, res|
|
|
79
|
-
res.status = 302
|
|
80
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
post "/form" do |req, res|
|
|
84
|
-
if "testing-form" == query_params(req)["example"]
|
|
85
|
-
res.status = 200
|
|
86
|
-
res.body = "passed :)"
|
|
87
|
-
else
|
|
88
|
-
res.status = 400
|
|
89
|
-
res.body = "invalid! >:E"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
post "/body" do |req, res|
|
|
94
|
-
if "testing-body" == request_body(req)
|
|
95
|
-
res.status = 200
|
|
96
|
-
res.body = "passed :)"
|
|
97
|
-
else
|
|
98
|
-
res.status = 400
|
|
99
|
-
res.body = "invalid! >:E"
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
head "/" do |_req, res|
|
|
104
|
-
res.status = 200
|
|
105
|
-
res["Content-Type"] = "text/html"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
get "/bytes" do |_req, res|
|
|
109
|
-
bytes = [80, 75, 3, 4, 20, 0, 0, 0, 8, 0, 123, 104, 169, 70, 99, 243, 243]
|
|
110
|
-
res["Content-Type"] = "application/octet-stream"
|
|
111
|
-
res.body = bytes.pack("c*")
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
get "/iso-8859-1" do |_req, res|
|
|
115
|
-
res["Content-Type"] = "text/plain; charset=ISO-8859-1"
|
|
116
|
-
res.body = "testæ".encode(Encoding::ISO8859_1)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
get "/cookies" do |req, res|
|
|
120
|
-
cookies = request_cookies(req)
|
|
121
|
-
res.cookies << SetCookie.new("foo", "bar")
|
|
122
|
-
res.body = cookies.map { |c| [c.name, c.value].join ": " }.join("\n")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
post "/echo-body" do |req, res|
|
|
126
|
-
res.status = 200
|
|
127
|
-
res.body = request_body(req)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
get "/héllö-wörld".b do |_req, res|
|
|
131
|
-
res.status = 200
|
|
132
|
-
res.body = "hello world"
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
get "/echo-cookies" do |req, res|
|
|
136
|
-
res.status = 200
|
|
137
|
-
cookies = request_cookies(req)
|
|
138
|
-
res.body = cookies.map { |c| "#{c.name}=#{c.value}" }.join("; ")
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
get "/redirect-with-cookie" do |_req, res|
|
|
142
|
-
res.status = 301
|
|
143
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/echo-cookies"
|
|
144
|
-
res.cookies << SetCookie.new("from_redirect", "yes", "/")
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
get "/redirect-cookie-chain/1" do |_req, res|
|
|
148
|
-
res.status = 301
|
|
149
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/redirect-cookie-chain/2"
|
|
150
|
-
res.cookies << SetCookie.new("first", "1", "/")
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
get "/redirect-cookie-chain/2" do |_req, res|
|
|
154
|
-
res.status = 301
|
|
155
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/echo-cookies"
|
|
156
|
-
res.cookies << SetCookie.new("second", "2", "/")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
get "/redirect-set-then-delete/1" do |_req, res|
|
|
160
|
-
res.status = 301
|
|
161
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/redirect-set-then-delete/2"
|
|
162
|
-
res.cookies << SetCookie.new("temp", "present", "/")
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
get "/redirect-set-then-delete/2" do |_req, res|
|
|
166
|
-
res.status = 301
|
|
167
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/echo-cookies"
|
|
168
|
-
res.cookies << SetCookie.new("temp", "", "/")
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
get "/redirect-no-cookies" do |_req, res|
|
|
172
|
-
res.status = 301
|
|
173
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/echo-cookies"
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
get "/cookie-loop" do |req, res|
|
|
177
|
-
cookies = request_cookies(req)
|
|
178
|
-
if cookies.any? { |c| c.name == "auth" && c.value == "ok" }
|
|
179
|
-
res.status = 200
|
|
180
|
-
res.body = "authenticated"
|
|
181
|
-
else
|
|
182
|
-
res.status = 302
|
|
183
|
-
res["Location"] = "http://#{server_addr}:#{server_port}/cookie-loop"
|
|
184
|
-
res.cookies << SetCookie.new("auth", "ok", "/")
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
get "/cross-origin-redirect" do |req, res|
|
|
189
|
-
target = query_params(req)["target"]
|
|
190
|
-
res.status = 302
|
|
191
|
-
res["Location"] = target
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
get "/cross-origin-redirect-with-cookie" do |req, res|
|
|
195
|
-
target = query_params(req)["target"]
|
|
196
|
-
res.status = 302
|
|
197
|
-
res["Location"] = target
|
|
198
|
-
res.cookies << SetCookie.new("from_origin", "yes", "/")
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "uri"
|
|
4
|
-
|
|
5
|
-
class DummyServer
|
|
6
|
-
class Servlet
|
|
7
|
-
def self.sockets
|
|
8
|
-
@sockets ||= []
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def initialize(server, memo)
|
|
12
|
-
@server = server
|
|
13
|
-
@memo = memo
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def service(req, res)
|
|
17
|
-
method = req.request_method.downcase
|
|
18
|
-
handler = self.class.routes["#{method}:#{req.path}"]
|
|
19
|
-
|
|
20
|
-
if handler
|
|
21
|
-
instance_exec(req, res, &handler)
|
|
22
|
-
else
|
|
23
|
-
res.status = 404
|
|
24
|
-
res.body = "#{req.unparsed_uri} not found"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
res["Connection"] = "keep-alive"
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class << self
|
|
31
|
-
def routes
|
|
32
|
-
@routes ||= {}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
%w[get post head].each do |method|
|
|
36
|
-
define_method(method) do |path, &block|
|
|
37
|
-
routes["#{method}:#{path}"] = block
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def request_body(req)
|
|
45
|
-
req.body
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def request_header(req, name)
|
|
49
|
-
req[name]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def request_cookies(req)
|
|
53
|
-
req.cookies
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def query_string(req)
|
|
57
|
-
req.query_string
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def query_params(req)
|
|
61
|
-
if req.body && req["Content-Type"]&.include?("application/x-www-form-urlencoded")
|
|
62
|
-
URI.decode_www_form(req.body).to_h
|
|
63
|
-
elsif req.query_string && !req.query_string.empty?
|
|
64
|
-
URI.decode_www_form(req.query_string).to_h
|
|
65
|
-
else
|
|
66
|
-
{}
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def server_addr
|
|
71
|
-
@server.addr
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def server_port
|
|
75
|
-
@server.port
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
require "support/dummy_server/routes"
|
|
81
|
-
require "support/dummy_server/encoding_routes"
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "openssl"
|
|
5
|
-
|
|
6
|
-
require "support/dummy_server/servlet"
|
|
7
|
-
require "support/servers/runner"
|
|
8
|
-
require "support/ssl_helper"
|
|
9
|
-
|
|
10
|
-
class DummyServer
|
|
11
|
-
def initialize(ssl: false)
|
|
12
|
-
@ssl = ssl
|
|
13
|
-
@tcp_server = TCPServer.new("127.0.0.1", 0)
|
|
14
|
-
@port = @tcp_server.addr[1]
|
|
15
|
-
@memo = {}
|
|
16
|
-
@servlet = Servlet.new(self, @memo)
|
|
17
|
-
@running = false
|
|
18
|
-
@ready = Queue.new
|
|
19
|
-
ssl_context if @ssl
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def addr
|
|
23
|
-
"127.0.0.1"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
attr_reader :port
|
|
27
|
-
|
|
28
|
-
def endpoint
|
|
29
|
-
"#{scheme}://#{addr}:#{port}"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def scheme
|
|
33
|
-
@ssl ? "https" : "http"
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def wait_ready
|
|
37
|
-
@ready.pop
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def start
|
|
41
|
-
server = @ssl ? OpenSSL::SSL::SSLServer.new(@tcp_server, ssl_context) : @tcp_server
|
|
42
|
-
@running = true
|
|
43
|
-
@ready << true
|
|
44
|
-
|
|
45
|
-
while @running
|
|
46
|
-
begin
|
|
47
|
-
client = server.accept
|
|
48
|
-
rescue OpenSSL::SSL::SSLError
|
|
49
|
-
next
|
|
50
|
-
end
|
|
51
|
-
Thread.new(client) { |c| handle_connection(c) }
|
|
52
|
-
end
|
|
53
|
-
rescue IOError, Errno::EBADF
|
|
54
|
-
# Server socket closed during shutdown
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def reset
|
|
58
|
-
@memo.clear
|
|
59
|
-
Servlet.sockets.clear
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def shutdown
|
|
63
|
-
@running = false
|
|
64
|
-
@tcp_server.close
|
|
65
|
-
rescue
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def ssl_context
|
|
70
|
-
@ssl_context ||= SSLHelper.server_context
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Simple HTTP request object for route handlers
|
|
74
|
-
Request = Struct.new(:request_method, :path, :query_string, :unparsed_uri,
|
|
75
|
-
:headers, :body, :socket) do
|
|
76
|
-
def [](name)
|
|
77
|
-
headers.each { |k, v| return v if k.casecmp?(name) }
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def cookies
|
|
82
|
-
cookie_header = self["Cookie"]
|
|
83
|
-
return [] unless cookie_header
|
|
84
|
-
|
|
85
|
-
cookie_header.split("; ").map do |pair|
|
|
86
|
-
name, value = pair.split("=", 2)
|
|
87
|
-
DummyServer::Cookie.new(name, value || "")
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Simple HTTP response object for route handlers
|
|
93
|
-
class Response
|
|
94
|
-
attr_accessor :status
|
|
95
|
-
attr_accessor :body
|
|
96
|
-
attr_accessor :cookies
|
|
97
|
-
|
|
98
|
-
def initialize
|
|
99
|
-
@status = 200
|
|
100
|
-
@body = ""
|
|
101
|
-
@headers = {}
|
|
102
|
-
@cookies = []
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def []=(name, value)
|
|
106
|
-
@headers[name] = value
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def [](name)
|
|
110
|
-
@headers[name]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def serialize(head_request: false)
|
|
114
|
-
lines = ["HTTP/1.1 #{status} #{STATUS_TEXT.fetch(status, 'Unknown')}"]
|
|
115
|
-
|
|
116
|
-
cookies.each do |cookie|
|
|
117
|
-
value = "#{cookie.name}=#{cookie.value}"
|
|
118
|
-
value += "; path=#{cookie.path}" if cookie.path
|
|
119
|
-
lines << "Set-Cookie: #{value}"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
@headers.each { |k, v| lines << "#{k}: #{v}" }
|
|
123
|
-
|
|
124
|
-
body_bytes = body.to_s.b
|
|
125
|
-
lines << "Content-Length: #{body_bytes.bytesize}" unless @headers.key?("Content-Length")
|
|
126
|
-
|
|
127
|
-
header_str = lines.join("\r\n") << "\r\n\r\n"
|
|
128
|
-
head_request ? header_str : header_str << body_bytes
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
STATUS_TEXT = {
|
|
132
|
-
200 => "OK", 204 => "No Content", 301 => "Moved Permanently",
|
|
133
|
-
302 => "Found", 400 => "Bad Request", 404 => "Not Found",
|
|
134
|
-
407 => "Proxy Authentication Required", 500 => "Internal Server Error"
|
|
135
|
-
}.freeze
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
Cookie = Struct.new(:name, :value)
|
|
139
|
-
SetCookie = Struct.new(:name, :value, :path)
|
|
140
|
-
|
|
141
|
-
private
|
|
142
|
-
|
|
143
|
-
def handle_connection(client)
|
|
144
|
-
loop do
|
|
145
|
-
request = read_request(client)
|
|
146
|
-
break unless request
|
|
147
|
-
|
|
148
|
-
response = Response.new
|
|
149
|
-
@servlet.service(request, response)
|
|
150
|
-
client.write(response.serialize(head_request: request.request_method == "HEAD"))
|
|
151
|
-
break unless response["Connection"]&.casecmp?("keep-alive")
|
|
152
|
-
end
|
|
153
|
-
rescue IOError, Errno::EBADF, Errno::ECONNRESET, Errno::EPIPE, Errno::EPROTOTYPE, OpenSSL::SSL::SSLError
|
|
154
|
-
# Connection closed or SSL error
|
|
155
|
-
ensure
|
|
156
|
-
client.close rescue nil
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def read_request(client)
|
|
160
|
-
line = client.gets
|
|
161
|
-
return unless line
|
|
162
|
-
|
|
163
|
-
method, uri, = line.split(" ", 3)
|
|
164
|
-
return bad_request(client) unless uri&.ascii_only?
|
|
165
|
-
|
|
166
|
-
raw_path, query_string = uri.split("?", 2)
|
|
167
|
-
headers = read_headers(client)
|
|
168
|
-
|
|
169
|
-
Request.new(
|
|
170
|
-
request_method: method, path: percent_decode(raw_path),
|
|
171
|
-
query_string: query_string, headers: headers,
|
|
172
|
-
body: read_body(client, headers), socket: client, unparsed_uri: uri
|
|
173
|
-
)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def read_headers(client)
|
|
177
|
-
headers = {}
|
|
178
|
-
while (header_line = client.gets)
|
|
179
|
-
break if header_line == "\r\n"
|
|
180
|
-
|
|
181
|
-
key, value = header_line.split(": ", 2)
|
|
182
|
-
headers[key] = value.strip if key && value
|
|
183
|
-
end
|
|
184
|
-
headers
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def read_body(client, headers)
|
|
188
|
-
content_length = headers["Content-Length"]
|
|
189
|
-
client.read(content_length.to_i) if content_length
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def bad_request(client)
|
|
193
|
-
client.write("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
|
|
194
|
-
nil
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def percent_decode(str)
|
|
198
|
-
str.b.gsub(/%([0-9A-Fa-f]{2})/) { [::Regexp.last_match(1)].pack("H2") }
|
|
199
|
-
end
|
|
200
|
-
end
|
data/test/support/fakeio.rb
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "stringio"
|
|
4
|
-
|
|
5
|
-
class FakeIO
|
|
6
|
-
def initialize(content)
|
|
7
|
-
@io = StringIO.new(content)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def string
|
|
11
|
-
@io.string
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def read(*)
|
|
15
|
-
@io.read(*)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def size
|
|
19
|
-
@io.size
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ConnectionReuseTests
|
|
4
|
-
# Including class must provide:
|
|
5
|
-
# - server: a DummyServer instance
|
|
6
|
-
# - build_client(**options): builds an HTTP::Client with given options
|
|
7
|
-
|
|
8
|
-
def test_connection_reuse_enabled_infers_host_from_persistent
|
|
9
|
-
client = build_client(persistent: server.endpoint)
|
|
10
|
-
|
|
11
|
-
assert_equal "<!doctype html>", client.get("/").body.to_s
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def test_connection_reuse_enabled_reuses_the_socket
|
|
15
|
-
client = build_client(persistent: server.endpoint)
|
|
16
|
-
sockets_used = [
|
|
17
|
-
client.get("#{server.endpoint}/socket/1").body.to_s,
|
|
18
|
-
client.get("#{server.endpoint}/socket/2").body.to_s
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
refute_includes sockets_used, ""
|
|
22
|
-
assert_equal 1, sockets_used.uniq.length
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def test_connection_reuse_enabled_mixed_state_reopens_connection
|
|
26
|
-
client = build_client(persistent: server.endpoint)
|
|
27
|
-
first_socket_id = client.get("#{server.endpoint}/socket/1").body.to_s
|
|
28
|
-
|
|
29
|
-
client.instance_variable_set(:@state, :dirty)
|
|
30
|
-
|
|
31
|
-
second_socket_id = client.get("#{server.endpoint}/socket/2").body.to_s
|
|
32
|
-
|
|
33
|
-
refute_equal first_socket_id, second_socket_id
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def test_connection_reuse_enabled_auto_flushes_unread_body
|
|
37
|
-
client = build_client(persistent: server.endpoint)
|
|
38
|
-
first_res = client.get(server.endpoint)
|
|
39
|
-
second_res = client.get(server.endpoint)
|
|
40
|
-
|
|
41
|
-
assert_equal "<!doctype html>", first_res.body.to_s
|
|
42
|
-
assert_equal "<!doctype html>", second_res.body.to_s
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def test_connection_reuse_enabled_reading_cached_body_succeeds
|
|
46
|
-
client = build_client(persistent: server.endpoint)
|
|
47
|
-
first_res = client.get(server.endpoint)
|
|
48
|
-
first_res.body.to_s
|
|
49
|
-
|
|
50
|
-
second_res = client.get(server.endpoint)
|
|
51
|
-
|
|
52
|
-
assert_equal "<!doctype html>", first_res.body.to_s
|
|
53
|
-
assert_equal "<!doctype html>", second_res.body.to_s
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def test_connection_reuse_enabled_socket_issue_transparently_reopens
|
|
57
|
-
client = build_client(persistent: server.endpoint)
|
|
58
|
-
first_socket_id = client.get("#{server.endpoint}/socket").body.to_s
|
|
59
|
-
|
|
60
|
-
refute_equal "", first_socket_id
|
|
61
|
-
# Kill off the sockets we used
|
|
62
|
-
DummyServer::Servlet.sockets.each do |socket|
|
|
63
|
-
socket.close
|
|
64
|
-
rescue IOError
|
|
65
|
-
nil
|
|
66
|
-
end
|
|
67
|
-
DummyServer::Servlet.sockets.clear
|
|
68
|
-
|
|
69
|
-
# Should error because we tried to use a bad socket
|
|
70
|
-
assert_raises(HTTP::ConnectionError) do
|
|
71
|
-
client.get("#{server.endpoint}/socket").body.to_s
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Should succeed since we create a new socket
|
|
75
|
-
second_socket_id = client.get("#{server.endpoint}/socket").body.to_s
|
|
76
|
-
|
|
77
|
-
refute_equal first_socket_id, second_socket_id
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def test_connection_reuse_enabled_change_in_host_errors
|
|
81
|
-
client = build_client(persistent: server.endpoint)
|
|
82
|
-
|
|
83
|
-
err = assert_raises(HTTP::StateError) { client.get("https://invalid.com/socket") }
|
|
84
|
-
assert_match(/Persistence is enabled/i, err.message)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def test_connection_reuse_disabled_opens_new_sockets
|
|
88
|
-
client = build_client
|
|
89
|
-
sockets_used = [
|
|
90
|
-
client.get("#{server.endpoint}/socket/1").body.to_s,
|
|
91
|
-
client.get("#{server.endpoint}/socket/2").body.to_s
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
refute_includes sockets_used, ""
|
|
95
|
-
assert_equal 2, sockets_used.uniq.length
|
|
96
|
-
end
|
|
97
|
-
end
|