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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +150 -28
  3. data/Rakefile +20 -31
  4. data/lib/http2.rb +105 -70
  5. data/lib/http2/base_request.rb +22 -0
  6. data/{include → lib/http2}/connection.rb +56 -28
  7. data/lib/http2/cookie.rb +22 -0
  8. data/lib/http2/errors.rb +18 -0
  9. data/lib/http2/get_request.rb +33 -0
  10. data/{include → lib/http2}/post_data_generator.rb +7 -6
  11. data/{include → lib/http2}/post_multipart_request.rb +20 -20
  12. data/{include → lib/http2}/post_request.rb +17 -22
  13. data/lib/http2/response.rb +109 -0
  14. data/{include → lib/http2}/response_reader.rb +75 -42
  15. data/{include → lib/http2}/url_builder.rb +6 -6
  16. data/lib/http2/utils.rb +52 -0
  17. data/spec/helpers.rb +27 -0
  18. data/spec/http2/cookies_spec.rb +18 -0
  19. data/spec/http2/get_request_spec.rb +11 -0
  20. data/spec/http2/post_data_generator_spec.rb +2 -1
  21. data/spec/http2/post_multipart_request_spec.rb +11 -0
  22. data/spec/http2/post_request_spec.rb +11 -0
  23. data/spec/http2/response_reader_spec.rb +12 -0
  24. data/spec/http2/response_spec.rb +73 -0
  25. data/spec/http2/url_builder_spec.rb +1 -1
  26. data/spec/http2_spec.rb +107 -89
  27. data/spec/spec_helper.rb +8 -8
  28. data/spec/spec_root/content_type_test.rhtml +4 -0
  29. data/spec/spec_root/cookie_test.rhtml +8 -0
  30. data/spec/spec_root/json_test.rhtml +9 -0
  31. data/spec/spec_root/multipart_test.rhtml +28 -0
  32. data/spec/spec_root/redirect_test.rhtml +3 -0
  33. data/spec/spec_root/unauthorized.rhtml +3 -0
  34. data/spec/spec_root/unsupported_media_type.rhtml +3 -0
  35. metadata +118 -36
  36. data/.document +0 -5
  37. data/.rspec +0 -1
  38. data/Gemfile +0 -14
  39. data/Gemfile.lock +0 -77
  40. data/VERSION +0 -1
  41. data/http2.gemspec +0 -77
  42. data/include/errors.rb +0 -11
  43. data/include/get_request.rb +0 -34
  44. data/include/response.rb +0 -73
  45. data/include/utils.rb +0 -40
  46. 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
- @response = Http2::Response.new(:request_args => args, :debug => @debug)
7
+ @request = request
8
+ @response = Http2::Response.new(debug: http2.debug, request: request)
8
9
  @rec_count = 0
9
- @args, @debug, @http2, @sock = args[:args], args[:http2].debug, args[:http2], args[:sock]
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 > 0
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
- if !@response.code
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 (@response.code == "302" || @response.code == "307") && @response.header?("location") && @http2.args[:follow_redirects]
78
+ if redirect_response?
78
79
  url, args = url_and_args_from_location
79
80
 
80
- if !args[:host] || args[:host] == @args[:host]
81
- return @http2.get(url)
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(@response.header("location"))
107
+ uri = URI.parse(url)
108
+
90
109
  url = uri.path
91
- url << "?#{uri.query}" if uri.query.to_s.length > 0
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
- return [url, args]
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", :invalid => :replace, :replace => "").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 @http2.raise_errors
144
+ return unless @http2.raise_errors
122
145
 
123
- if @response.code == "500"
146
+ case @response.code
147
+ when "500"
124
148
  err = Http2::Errors::Internalserver.new("A internal server error occurred")
125
- elsif @response.code == "403"
149
+ when "403"
126
150
  err = Http2::Errors::Noaccess.new("No access")
127
- elsif @response.code == "400"
151
+ when "400"
128
152
  err = Http2::Errors::Badrequest.new("Bad request")
129
- elsif @response.code == "404"
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
- raise Errno::ECONNABORTED, "Server closed the connection before being able to read anything (KeepAliveMax: '#{@http2.keepalive_max}', Connection: '#{@connection}', PID: '#{Process.pid}')."
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 |cookie_data|
149
- @http2.cookies[cookie_data["name"]] = cookie_data
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 = @content_type_line
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 == 0
254
+ return :break if @length&.zero?
223
255
 
224
256
  if @transfer_encoding == "chunked"
225
- return parse_body_chunked(line)
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 > 0
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 == 0
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 args = {}
4
+ def initialize
5
5
  @params = {}
6
6
  end
7
7
 
8
8
  def build_params
9
9
  url_params = ""
10
10
 
11
- if !params.empty?
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
- return url_params
27
+ url_params
28
28
  end
29
29
 
30
30
  def build_path_and_params
31
- url = "#{path}"
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
- return url
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
- return url
52
+ url
53
53
  end
54
54
 
55
55
  def params?
@@ -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