http2 0.0.28 → 0.0.33
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 +5 -5
- data/README.md +150 -28
- data/Rakefile +20 -31
- data/lib/http2.rb +105 -70
- data/lib/http2/base_request.rb +22 -0
- data/{include → lib/http2}/connection.rb +56 -28
- data/lib/http2/cookie.rb +22 -0
- data/lib/http2/errors.rb +18 -0
- data/lib/http2/get_request.rb +33 -0
- data/{include → lib/http2}/post_data_generator.rb +7 -6
- data/{include → lib/http2}/post_multipart_request.rb +20 -20
- data/{include → lib/http2}/post_request.rb +17 -22
- data/lib/http2/response.rb +109 -0
- data/{include → lib/http2}/response_reader.rb +75 -42
- data/{include → lib/http2}/url_builder.rb +6 -6
- data/lib/http2/utils.rb +52 -0
- data/spec/helpers.rb +27 -0
- data/spec/http2/cookies_spec.rb +18 -0
- data/spec/http2/get_request_spec.rb +11 -0
- data/spec/http2/post_data_generator_spec.rb +2 -1
- data/spec/http2/post_multipart_request_spec.rb +11 -0
- data/spec/http2/post_request_spec.rb +11 -0
- data/spec/http2/response_reader_spec.rb +12 -0
- data/spec/http2/response_spec.rb +73 -0
- data/spec/http2/url_builder_spec.rb +1 -1
- data/spec/http2_spec.rb +107 -89
- data/spec/spec_helper.rb +8 -8
- data/spec/spec_root/content_type_test.rhtml +4 -0
- data/spec/spec_root/cookie_test.rhtml +8 -0
- data/spec/spec_root/json_test.rhtml +9 -0
- data/spec/spec_root/multipart_test.rhtml +28 -0
- data/spec/spec_root/redirect_test.rhtml +3 -0
- data/spec/spec_root/unauthorized.rhtml +3 -0
- data/spec/spec_root/unsupported_media_type.rhtml +3 -0
- metadata +118 -36
- data/.document +0 -5
- data/.rspec +0 -1
- data/Gemfile +0 -14
- data/Gemfile.lock +0 -77
- data/VERSION +0 -1
- data/http2.gemspec +0 -77
- data/include/errors.rb +0 -11
- data/include/get_request.rb +0 -34
- data/include/response.rb +0 -73
- data/include/utils.rb +0 -40
- data/shippable.yml +0 -7
@@ -0,0 +1,109 @@
|
|
1
|
+
# This object will be returned as the response for each request.
|
2
|
+
class Http2::Response
|
3
|
+
# All the data the response contains. Headers, body, cookies, requested URL and more.
|
4
|
+
attr_reader :headers, :request, :request_args, :requested_url
|
5
|
+
attr_accessor :body, :charset, :code, :http_version
|
6
|
+
attr_writer :content_type
|
7
|
+
|
8
|
+
# This method should not be called manually.
|
9
|
+
def initialize(body: "", debug: false, headers: {}, request:)
|
10
|
+
@body = body
|
11
|
+
@debug = debug
|
12
|
+
@headers = headers
|
13
|
+
@request = request
|
14
|
+
@requested_url = request.path
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a certain header by name or false if not found.
|
18
|
+
#===Examples
|
19
|
+
# val = res.header("content-type")
|
20
|
+
def header(key)
|
21
|
+
return false unless headers.key?(key)
|
22
|
+
|
23
|
+
headers.fetch(key).first.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if a header of the given string exists.
|
27
|
+
#===Examples
|
28
|
+
# print "No content-type was given." if !http.header?("content-type")
|
29
|
+
def header?(key)
|
30
|
+
return true if headers.key?(key) && !headers[key].first.to_s.empty?
|
31
|
+
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_length
|
36
|
+
if header?("content-length")
|
37
|
+
header("content-length").to_i
|
38
|
+
elsif @body
|
39
|
+
@body.bytesize
|
40
|
+
else
|
41
|
+
raise "Couldn't calculate content-length."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def content_type
|
46
|
+
if header?("content-type")
|
47
|
+
header("content-type")
|
48
|
+
else
|
49
|
+
raise "No content-type was given."
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks the data that has been sat on the object and raises various exceptions, if it does not validate somehow.
|
54
|
+
def validate!
|
55
|
+
puts "Http2: Validating response length." if @debug
|
56
|
+
validate_body_versus_content_length!
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true if the result is JSON.
|
60
|
+
def json?
|
61
|
+
content_type == "application/json"
|
62
|
+
end
|
63
|
+
|
64
|
+
def json
|
65
|
+
@json ||= JSON.parse(body)
|
66
|
+
end
|
67
|
+
|
68
|
+
def host
|
69
|
+
@request.http2.host
|
70
|
+
end
|
71
|
+
|
72
|
+
def port
|
73
|
+
@request.http2.port
|
74
|
+
end
|
75
|
+
|
76
|
+
def ssl?
|
77
|
+
@request.http2.ssl?
|
78
|
+
end
|
79
|
+
|
80
|
+
def path
|
81
|
+
@request.path
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
"#<Http::Response host=\"#{host}\" port=#{port} ssl=#{ssl?} path=\"#{path}\">"
|
86
|
+
end
|
87
|
+
|
88
|
+
def inspect
|
89
|
+
to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Checks that the length of the body is the same as the given content-length if given.
|
95
|
+
def validate_body_versus_content_length!
|
96
|
+
unless header?("content-length")
|
97
|
+
puts "Http2: No content length given - skipping length validation." if @debug
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
|
101
|
+
content_length = header("content-length").to_i
|
102
|
+
body_length = @body.bytesize
|
103
|
+
|
104
|
+
puts "Http2: Body length: #{body_length}" if @debug
|
105
|
+
puts "Http2: Content length: #{content_length}" if @debug
|
106
|
+
|
107
|
+
raise "Body does not match the given content-length: '#{body_length}', '#{content_length}'." if body_length != content_length
|
108
|
+
end
|
109
|
+
end
|
@@ -1,17 +1,21 @@
|
|
1
1
|
class Http2::ResponseReader
|
2
2
|
attr_reader :response
|
3
3
|
|
4
|
-
def initialize(args)
|
4
|
+
def initialize(args:, http2:, sock:, request:)
|
5
5
|
@mode = "headers"
|
6
6
|
@transfer_encoding = nil
|
7
|
-
@
|
7
|
+
@request = request
|
8
|
+
@response = Http2::Response.new(debug: http2.debug, request: request)
|
8
9
|
@rec_count = 0
|
9
|
-
@args
|
10
|
+
@args = args
|
11
|
+
@debug = http2.debug
|
12
|
+
@http2 = http2
|
13
|
+
@sock = sock
|
10
14
|
@nl = @http2.nl
|
11
15
|
@conn = @http2.connection
|
12
16
|
|
13
17
|
read_headers
|
14
|
-
read_body if @length == nil || @length
|
18
|
+
read_body if @length == nil || @length.positive?
|
15
19
|
finish
|
16
20
|
end
|
17
21
|
|
@@ -23,6 +27,7 @@ class Http2::ResponseReader
|
|
23
27
|
if line == "\n" || line == "\r\n" || line == @nl
|
24
28
|
puts "Http2: Changing mode to body!" if @debug
|
25
29
|
raise "No headers was given at all? Possibly corrupt state after last request?" if @response.headers.empty?
|
30
|
+
|
26
31
|
@mode = "body"
|
27
32
|
@http2.on_content_call(@args, @nl)
|
28
33
|
break
|
@@ -49,24 +54,20 @@ class Http2::ResponseReader
|
|
49
54
|
end
|
50
55
|
|
51
56
|
def finish
|
52
|
-
#Check if we should reconnect based on keep-alive-max.
|
53
|
-
if @keepalive_max == 1 || @connection == "close"
|
54
|
-
@conn.close unless @conn.closed?
|
55
|
-
end
|
57
|
+
# Check if we should reconnect based on keep-alive-max.
|
58
|
+
@conn.close if !@conn.closed? && (@keepalive_max == 1 || @connection == "close")
|
56
59
|
|
57
60
|
# Validate that the response is as it should be.
|
58
61
|
puts "Http2: Validating response." if @debug
|
59
62
|
|
60
|
-
|
61
|
-
raise "No status-code was received from the server. Headers: '#{@response.headers}' Body: '#{resp.body}'."
|
62
|
-
end
|
63
|
+
raise "No status-code was received from the server. Headers: '#{@response.headers}' Body: '#{@response.body}'." unless @response.code
|
63
64
|
|
64
65
|
@response.validate!
|
65
66
|
check_and_decode
|
66
67
|
@http2.autostate_register(@response) if @http2.args[:autostate]
|
67
68
|
handle_errors
|
68
69
|
|
69
|
-
if response = check_and_follow_redirect
|
70
|
+
if (response = check_and_follow_redirect)
|
70
71
|
@response = response
|
71
72
|
end
|
72
73
|
end
|
@@ -74,27 +75,49 @@ class Http2::ResponseReader
|
|
74
75
|
private
|
75
76
|
|
76
77
|
def check_and_follow_redirect
|
77
|
-
if
|
78
|
+
if redirect_response?
|
78
79
|
url, args = url_and_args_from_location
|
79
80
|
|
80
|
-
if
|
81
|
-
|
81
|
+
if redirect_using_same_connection?(args)
|
82
|
+
@http2.get(url)
|
82
83
|
else
|
83
84
|
::Http2.new(args).get(url)
|
84
85
|
end
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
89
|
+
REDIRECT_CODES = [301, 302, 303, 307, 308].freeze
|
90
|
+
def redirect_response?
|
91
|
+
REDIRECT_CODES.include?(response.code.to_i) && response.header?("location") && @http2.args[:follow_redirects]
|
92
|
+
end
|
93
|
+
|
94
|
+
def redirect_using_same_connection?(args)
|
95
|
+
if !args[:host] || args[:host] == @args[:host]
|
96
|
+
true
|
97
|
+
else
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def url
|
103
|
+
@url ||= response.header("location")
|
104
|
+
end
|
105
|
+
|
88
106
|
def url_and_args_from_location
|
89
|
-
uri = URI.parse(
|
107
|
+
uri = URI.parse(url)
|
108
|
+
|
90
109
|
url = uri.path
|
91
|
-
url << "?#{uri.query}"
|
110
|
+
url << "?#{uri.query}" unless uri.query.to_s.empty?
|
111
|
+
url = url.gsub(/\A\//, "")
|
112
|
+
|
113
|
+
args = @http2.args
|
114
|
+
.reject { |k, _v| [:ssl, :port].include? k }
|
115
|
+
.merge(host: uri.host)
|
92
116
|
|
93
|
-
args = {host: uri.host}
|
94
117
|
args[:ssl] = true if uri.scheme == "https"
|
95
118
|
args[:port] = uri.port if uri.port
|
96
119
|
|
97
|
-
|
120
|
+
[url, args]
|
98
121
|
end
|
99
122
|
|
100
123
|
def check_and_decode
|
@@ -109,8 +132,8 @@ private
|
|
109
132
|
|
110
133
|
begin
|
111
134
|
valid_string = ic.encode("UTF-8")
|
112
|
-
rescue
|
113
|
-
valid_string = untrusted_str.force_encoding("UTF-8").encode("UTF-8", :
|
135
|
+
rescue StandardError
|
136
|
+
valid_string = untrusted_str.force_encoding("UTF-8").encode("UTF-8", invalid: :replace, replace: "").encode("UTF-8")
|
114
137
|
end
|
115
138
|
|
116
139
|
@response.body = valid_string
|
@@ -118,16 +141,21 @@ private
|
|
118
141
|
end
|
119
142
|
|
120
143
|
def handle_errors
|
121
|
-
return unless
|
144
|
+
return unless @http2.raise_errors
|
122
145
|
|
123
|
-
|
146
|
+
case @response.code
|
147
|
+
when "500"
|
124
148
|
err = Http2::Errors::Internalserver.new("A internal server error occurred")
|
125
|
-
|
149
|
+
when "403"
|
126
150
|
err = Http2::Errors::Noaccess.new("No access")
|
127
|
-
|
151
|
+
when "400"
|
128
152
|
err = Http2::Errors::Badrequest.new("Bad request")
|
129
|
-
|
153
|
+
when "401"
|
154
|
+
err = Http2::Errors::Unauthorized.new("Unauthorized")
|
155
|
+
when "404"
|
130
156
|
err = Http2::Errors::Notfound.new("Not found")
|
157
|
+
when "415"
|
158
|
+
err = Http2::Errors::UnsupportedMediaType.new("Unsupported media type")
|
131
159
|
end
|
132
160
|
|
133
161
|
if err
|
@@ -140,13 +168,19 @@ private
|
|
140
168
|
if line
|
141
169
|
@rec_count += line.length
|
142
170
|
elsif !line && @rec_count <= 0
|
143
|
-
|
171
|
+
parts = [
|
172
|
+
"KeepAliveMax: '#{@http2.keepalive_max}'",
|
173
|
+
"Connection: '#{@connection}'",
|
174
|
+
"PID: '#{Process.pid}'"
|
175
|
+
]
|
176
|
+
|
177
|
+
raise Errno::ECONNABORTED, "Server closed the connection before being able to read anything (#{parts.join(", ")})."
|
144
178
|
end
|
145
179
|
end
|
146
180
|
|
147
181
|
def parse_cookie(cookie_line)
|
148
|
-
::Http2::Utils.parse_set_cookies(cookie_line).each do |
|
149
|
-
@http2.cookies[
|
182
|
+
::Http2::Utils.parse_set_cookies(cookie_line).each do |cookie|
|
183
|
+
@http2.cookies[cookie.name] = cookie
|
150
184
|
end
|
151
185
|
end
|
152
186
|
|
@@ -163,24 +197,24 @@ private
|
|
163
197
|
end
|
164
198
|
|
165
199
|
def parse_content_type(content_type_line)
|
166
|
-
if match_charset = content_type_line.match(/\s*;\s*charset=(.+)/i)
|
200
|
+
if (match_charset = content_type_line.match(/\s*;\s*charset=(.+)/i))
|
167
201
|
@charset = match_charset[1].downcase
|
168
202
|
@response.charset = @charset
|
169
203
|
content_type_line.gsub!(match_charset[0], "")
|
170
204
|
end
|
171
205
|
|
172
|
-
@response.content_type =
|
206
|
+
@response.content_type = content_type_line
|
173
207
|
end
|
174
208
|
|
175
|
-
#Parse a header-line and saves it on the object.
|
209
|
+
# Parse a header-line and saves it on the object.
|
176
210
|
#===Examples
|
177
211
|
# http.parse_header("Content-Type: text/html\r\n")
|
178
212
|
def parse_header(line)
|
179
|
-
if match = line.match(/^(.+?):\s*(.+)#{@nl}$/)
|
213
|
+
if (match = line.match(/^(.+?):\s*(.+)#{@nl}$/))
|
180
214
|
key = match[1].downcase
|
181
215
|
set_header_special_values(key, match[2])
|
182
216
|
parse_normal_header(line, key, match[1], match[2])
|
183
|
-
elsif match = line.match(/^HTTP\/([\d\.]+)\s+(\d+)\s+(.+)$/)
|
217
|
+
elsif (match = line.match(/^HTTP\/([\d\.]+)\s+(\d+)\s+(.+)$/)) # rubocop:disable Style/RedundantRegexpEscape
|
184
218
|
@response.code = match[2]
|
185
219
|
@response.http_version = match[1]
|
186
220
|
@http2.on_content_call(@args, line)
|
@@ -211,18 +245,16 @@ private
|
|
211
245
|
@response.headers[key] = [] unless @response.headers.key?(key)
|
212
246
|
@response.headers[key] << value
|
213
247
|
|
214
|
-
if key != "transfer-encoding" && key != "content-length" && key != "connection" && key != "keep-alive"
|
215
|
-
@http2.on_content_call(@args, line)
|
216
|
-
end
|
248
|
+
@http2.on_content_call(@args, line) if key != "transfer-encoding" && key != "content-length" && key != "connection" && key != "keep-alive"
|
217
249
|
end
|
218
250
|
|
219
|
-
#Parses the body based on given headers and saves it to the result-object.
|
251
|
+
# Parses the body based on given headers and saves it to the result-object.
|
220
252
|
# http.parse_body(str)
|
221
253
|
def parse_body(line)
|
222
|
-
return :break if @length
|
254
|
+
return :break if @length&.zero?
|
223
255
|
|
224
256
|
if @transfer_encoding == "chunked"
|
225
|
-
|
257
|
+
parse_body_chunked(line)
|
226
258
|
else
|
227
259
|
puts "Http2: Adding #{line.to_s.bytesize} to the body." if @debug
|
228
260
|
@response.body << line
|
@@ -234,15 +266,16 @@ private
|
|
234
266
|
def parse_body_chunked(line)
|
235
267
|
len = line.strip.hex
|
236
268
|
|
237
|
-
if len
|
269
|
+
if len.positive?
|
238
270
|
read = @conn.read(len)
|
239
271
|
return :break if read == "" || read == "\n" || read == "\r\n"
|
272
|
+
|
240
273
|
@response.body << read
|
241
274
|
@http2.on_content_call(@args, read)
|
242
275
|
end
|
243
276
|
|
244
277
|
nl = @conn.gets
|
245
|
-
if len
|
278
|
+
if len.zero?
|
246
279
|
if nl == "\n" || nl == "\r\n"
|
247
280
|
return :break
|
248
281
|
else
|
@@ -1,14 +1,14 @@
|
|
1
1
|
class Http2::UrlBuilder
|
2
2
|
attr_accessor :host, :port, :protocol, :path, :params
|
3
3
|
|
4
|
-
def initialize
|
4
|
+
def initialize
|
5
5
|
@params = {}
|
6
6
|
end
|
7
7
|
|
8
8
|
def build_params
|
9
9
|
url_params = ""
|
10
10
|
|
11
|
-
|
11
|
+
unless params.empty?
|
12
12
|
first = true
|
13
13
|
|
14
14
|
params.each do |key, val|
|
@@ -24,18 +24,18 @@ class Http2::UrlBuilder
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
|
27
|
+
url_params
|
28
28
|
end
|
29
29
|
|
30
30
|
def build_path_and_params
|
31
|
-
url =
|
31
|
+
url = path.to_s
|
32
32
|
|
33
33
|
if params?
|
34
34
|
url << "?"
|
35
35
|
url << build_params
|
36
36
|
end
|
37
37
|
|
38
|
-
|
38
|
+
url
|
39
39
|
end
|
40
40
|
|
41
41
|
def build
|
@@ -49,7 +49,7 @@ class Http2::UrlBuilder
|
|
49
49
|
|
50
50
|
url << build_path_and_params
|
51
51
|
|
52
|
-
|
52
|
+
url
|
53
53
|
end
|
54
54
|
|
55
55
|
def params?
|
data/lib/http2/utils.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# This class holds various methods for encoding, decoding and parsing of HTTP-related stuff.
|
2
|
+
class Http2::Utils
|
3
|
+
# URL-encodes a string.
|
4
|
+
def self.urlenc(string)
|
5
|
+
# Thanks to CGI framework
|
6
|
+
string.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/) do
|
7
|
+
"%#{Regexp.last_match(1).unpack("H2" * Regexp.last_match(1).bytesize).join("%").upcase}"
|
8
|
+
end.tr(" ", "+")
|
9
|
+
end
|
10
|
+
|
11
|
+
# URL-decodes a string.
|
12
|
+
def self.urldec(string)
|
13
|
+
# Thanks to CGI framework
|
14
|
+
string.to_s.tr("+", " ").gsub(/((?:%[0-9a-fA-F]{2})+)/) do
|
15
|
+
[Regexp.last_match(1).delete("%")].pack("H*")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parses a cookies-string and returns them in an array.
|
20
|
+
def self.parse_set_cookies(str)
|
21
|
+
str = str.to_s
|
22
|
+
return [] if str.empty?
|
23
|
+
|
24
|
+
cookie_start_regex = /^(.+?)=(.*?)(;\s*|$)/
|
25
|
+
|
26
|
+
match = str.match(cookie_start_regex)
|
27
|
+
raise "Could not match cookie: '#{str}'" unless match
|
28
|
+
|
29
|
+
str.gsub!(cookie_start_regex, "")
|
30
|
+
|
31
|
+
cookie_data = {
|
32
|
+
name: urldec(match[1].to_s),
|
33
|
+
value: urldec(match[2].to_s)
|
34
|
+
}
|
35
|
+
|
36
|
+
while (match = str.match(/(.+?)=(.*?)(;\s*|$)/))
|
37
|
+
str = str.gsub(match[0], "")
|
38
|
+
key = match[1].to_s.downcase
|
39
|
+
value = match[2].to_s
|
40
|
+
|
41
|
+
if key == "path" || key == "expires"
|
42
|
+
cookie_data[key.to_sym] = value
|
43
|
+
else
|
44
|
+
cookie_data[key] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
cookie = Http2::Cookie.new(cookie_data)
|
49
|
+
|
50
|
+
[cookie]
|
51
|
+
end
|
52
|
+
end
|