http2 0.0.28 → 0.0.33

Sign up to get free protection for your applications and to get access to all the features.
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