http2 0.0.24 → 0.0.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -3
- data/Gemfile.lock +25 -19
- data/README.md +1 -0
- data/VERSION +1 -1
- data/http2.gemspec +16 -8
- data/include/connection.rb +142 -0
- data/include/get_request.rb +34 -0
- data/include/post_data_generator.rb +62 -0
- data/include/post_multipart_request.rb +113 -0
- data/include/post_request.rb +71 -0
- data/include/response.rb +17 -45
- data/include/response_reader.rb +101 -97
- data/include/url_builder.rb +58 -0
- data/lib/http2.rb +52 -315
- data/spec/http2/post_data_generator_spec.rb +24 -0
- data/spec/http2/url_builder_spec.rb +17 -0
- data/spec/http2_spec.rb +18 -24
- data/spec/spec_helper.rb +3 -1
- metadata +25 -18
- data/include/post_multipart_helper.rb +0 -77
@@ -0,0 +1,71 @@
|
|
1
|
+
class Http2::PostRequest
|
2
|
+
VALID_ARGUMENTS_POST = [:post, :url, :default_headers, :headers, :json, :method, :cookies, :on_content, :content_type]
|
3
|
+
|
4
|
+
def initialize(http2, args)
|
5
|
+
args.each do |key, val|
|
6
|
+
raise "Invalid key: '#{key}'." unless VALID_ARGUMENTS_POST.include?(key)
|
7
|
+
end
|
8
|
+
|
9
|
+
@http2, @args, @debug, @nl = http2, http2.parse_args(args), http2.debug, http2.nl
|
10
|
+
@conn = @http2.connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
@data = raw_data
|
15
|
+
|
16
|
+
@http2.mutex.synchronize do
|
17
|
+
puts "Http2: Doing post." if @debug
|
18
|
+
puts "Http2: Header str: #{header_str}" if @debug
|
19
|
+
|
20
|
+
@conn.write(header_string)
|
21
|
+
return @http2.read_response(@args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def method
|
28
|
+
if @args[:method]
|
29
|
+
@args[:method].to_s.upcase
|
30
|
+
else
|
31
|
+
"POST"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_type
|
36
|
+
if @args[:content_type]
|
37
|
+
@args[:content_type]
|
38
|
+
elsif @args[:json]
|
39
|
+
content_type = "application/json"
|
40
|
+
else
|
41
|
+
"application/x-www-form-urlencoded"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def raw_data
|
46
|
+
if @args[:json]
|
47
|
+
require "json" unless ::Kernel.const_defined?(:JSON)
|
48
|
+
@args[:json].to_json
|
49
|
+
elsif @args[:post].is_a?(String)
|
50
|
+
@args[:post]
|
51
|
+
else
|
52
|
+
phash = @args[:post] ? @args[:post].clone : {}
|
53
|
+
@http2.autostate_set_on_post_hash(phash) if @http2.args[:autostate]
|
54
|
+
::Http2::PostDataGenerator.new(phash).generate
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def headers
|
59
|
+
headers_hash = {"Content-Length" => @data.bytesize, "Content-Type" => content_type}
|
60
|
+
headers_hash.merge! @http2.default_headers(@args)
|
61
|
+
end
|
62
|
+
|
63
|
+
def header_string
|
64
|
+
header_str = "#{method} /#{@args[:url]} HTTP/1.1#{@nl}"
|
65
|
+
header_str << @http2.header_str(headers, @args)
|
66
|
+
header_str << @nl
|
67
|
+
header_str << @data
|
68
|
+
|
69
|
+
header_str
|
70
|
+
end
|
71
|
+
end
|
data/include/response.rb
CHANGED
@@ -2,22 +2,23 @@
|
|
2
2
|
class Http2::Response
|
3
3
|
#All the data the response contains. Headers, body, cookies, requested URL and more.
|
4
4
|
attr_reader :args
|
5
|
-
|
5
|
+
attr_accessor :body, :charset, :code, :content_type, :http_version
|
6
|
+
|
6
7
|
#This method should not be called manually.
|
7
8
|
def initialize(args = {})
|
8
9
|
@args = args
|
9
10
|
@args[:headers] = {} if !@args.key?(:headers)
|
10
|
-
@args[:body]
|
11
|
+
@body = args[:body] || ""
|
11
12
|
@debug = @args[:debug]
|
12
13
|
end
|
13
|
-
|
14
|
+
|
14
15
|
#Returns headers given from the host for the result.
|
15
16
|
#===Examples
|
16
17
|
# headers_hash = res.headers
|
17
18
|
def headers
|
18
19
|
return @args[:headers]
|
19
20
|
end
|
20
|
-
|
21
|
+
|
21
22
|
#Returns a certain header by name or false if not found.
|
22
23
|
#===Examples
|
23
24
|
# val = res.header("content-type")
|
@@ -25,7 +26,7 @@ class Http2::Response
|
|
25
26
|
return false if !@args[:headers].key?(key)
|
26
27
|
return @args[:headers][key].first.to_s
|
27
28
|
end
|
28
|
-
|
29
|
+
|
29
30
|
#Returns true if a header of the given string exists.
|
30
31
|
#===Examples
|
31
32
|
# print "No content-type was given." if !http.header?("content-type")
|
@@ -33,40 +34,11 @@ class Http2::Response
|
|
33
34
|
return true if @args[:headers].key?(key) and @args[:headers][key].first.to_s.length > 0
|
34
35
|
return false
|
35
36
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# print "An internal error occurred." if res.code.to_i == 500
|
40
|
-
def code
|
41
|
-
return @args[:code]
|
42
|
-
end
|
43
|
-
|
44
|
-
#Returns the HTTP-version of the result.
|
45
|
-
#===Examples
|
46
|
-
# print "We are using HTTP 1.1 and should support keep-alive." if res.http_version.to_s == "1.1"
|
47
|
-
def http_version
|
48
|
-
return @args[:http_version]
|
49
|
-
end
|
50
|
-
|
51
|
-
#Returns the complete body of the result as a string.
|
52
|
-
#===Examples
|
53
|
-
# print "Looks like we caught the end of it as well?" if res.body.to_s.downcase.index("</html>") != nil
|
54
|
-
def body
|
55
|
-
return @args[:body]
|
56
|
-
end
|
57
|
-
|
58
|
-
#Returns the charset of the result.
|
59
|
-
def charset
|
60
|
-
return @args[:charset]
|
61
|
-
end
|
62
|
-
|
63
|
-
#Returns the content-type of the result as a string.
|
64
|
-
#===Examples
|
65
|
-
# print "This body can be printed - its just plain text!" if http.contenttype == "text/plain"
|
66
|
-
def contenttype
|
67
|
-
return @args[:contenttype]
|
37
|
+
|
38
|
+
def content_length
|
39
|
+
header("content-length").to_i if header?("content-length")
|
68
40
|
end
|
69
|
-
|
41
|
+
|
70
42
|
#Returns the requested URL as a string.
|
71
43
|
#===Examples
|
72
44
|
# res.requested_url #=> "?show=status&action=getstatus"
|
@@ -74,28 +46,28 @@ class Http2::Response
|
|
74
46
|
raise "URL could not be detected." if !@args[:request_args][:url]
|
75
47
|
return @args[:request_args][:url]
|
76
48
|
end
|
77
|
-
|
49
|
+
|
78
50
|
# Checks the data that has been sat on the object and raises various exceptions, if it does not validate somehow.
|
79
51
|
def validate!
|
80
52
|
puts "Http2: Validating response length." if @debug
|
81
53
|
validate_body_versus_content_length!
|
82
54
|
end
|
83
|
-
|
55
|
+
|
84
56
|
private
|
85
|
-
|
57
|
+
|
86
58
|
# Checks that the length of the body is the same as the given content-length if given.
|
87
59
|
def validate_body_versus_content_length!
|
88
60
|
unless self.header?("content-length")
|
89
61
|
puts "Http2: No content length given - skipping length validation." if @debug
|
90
62
|
return nil
|
91
63
|
end
|
92
|
-
|
64
|
+
|
93
65
|
content_length = self.header("content-length").to_i
|
94
|
-
body_length = @
|
95
|
-
|
66
|
+
body_length = @body.bytesize
|
67
|
+
|
96
68
|
puts "Http2: Body length: #{body_length}" if @debug
|
97
69
|
puts "Http2: Content length: #{content_length}" if @debug
|
98
|
-
|
70
|
+
|
99
71
|
raise "Body does not match the given content-length: '#{body_length}', '#{content_length}'." if body_length != content_length
|
100
72
|
end
|
101
73
|
end
|
data/include/response_reader.rb
CHANGED
@@ -6,23 +6,23 @@ class Http2::ResponseReader
|
|
6
6
|
@transfer_encoding = nil
|
7
7
|
@response = Http2::Response.new(:request_args => args, :debug => @debug)
|
8
8
|
@rec_count = 0
|
9
|
-
@args, @debug, @http2, @sock = args[:args], args[:debug
|
9
|
+
@args, @debug, @http2, @sock = args[:args], args[:http2].debug, args[:http2], args[:sock]
|
10
10
|
@nl = @http2.nl
|
11
|
+
@conn = @http2.connection
|
11
12
|
|
12
13
|
read_headers
|
13
|
-
read_body
|
14
|
+
read_body if @length == nil || @length > 0
|
14
15
|
finish
|
15
16
|
end
|
16
17
|
|
17
18
|
def read_headers
|
18
19
|
loop do
|
19
|
-
line = @
|
20
|
+
line = @conn.gets
|
20
21
|
check_line_read(line)
|
21
22
|
|
22
23
|
if line == "\n" || line == "\r\n" || line == @nl
|
23
24
|
puts "Http2: Changing mode to body!" if @debug
|
24
25
|
raise "No headers was given at all? Possibly corrupt state after last request?" if @response.headers.empty?
|
25
|
-
break if @length == 0
|
26
26
|
@mode = "body"
|
27
27
|
@http2.on_content_call(@args, @nl)
|
28
28
|
break
|
@@ -34,71 +34,76 @@ class Http2::ResponseReader
|
|
34
34
|
|
35
35
|
def read_body
|
36
36
|
loop do
|
37
|
-
if @length
|
38
|
-
line = @
|
37
|
+
if @length
|
38
|
+
line = @conn.read(@length)
|
39
39
|
raise "Expected to get #{@length} of bytes but got #{line.bytesize}" if @length != line.bytesize
|
40
40
|
else
|
41
|
-
line = @
|
41
|
+
line = @conn.gets
|
42
42
|
end
|
43
43
|
|
44
44
|
check_line_read(line)
|
45
45
|
stat = parse_body(line)
|
46
|
-
break if stat ==
|
47
|
-
next if stat ==
|
46
|
+
break if stat == :break
|
47
|
+
next if stat == :next
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
def finish
|
52
52
|
#Check if we should reconnect based on keep-alive-max.
|
53
|
-
if @keepalive_max == 1
|
54
|
-
@
|
53
|
+
if @keepalive_max == 1 || @connection == "close"
|
54
|
+
@conn.close unless @conn.closed?
|
55
55
|
end
|
56
56
|
|
57
57
|
# Validate that the response is as it should be.
|
58
58
|
puts "Http2: Validating response." if @debug
|
59
59
|
|
60
|
-
if !@response.
|
61
|
-
raise "No status-code was received from the server. Headers: '#{@response.headers}' Body: '#{resp.
|
60
|
+
if !@response.code
|
61
|
+
raise "No status-code was received from the server. Headers: '#{@response.headers}' Body: '#{resp.body}'."
|
62
62
|
end
|
63
63
|
|
64
64
|
@response.validate!
|
65
65
|
check_and_decode
|
66
|
-
|
66
|
+
@http2.autostate_register(@response) if @http2.args[:autostate]
|
67
67
|
handle_errors
|
68
68
|
|
69
|
-
|
69
|
+
if response = check_and_follow_redirect
|
70
|
+
@response = response
|
71
|
+
end
|
70
72
|
end
|
71
73
|
|
72
74
|
private
|
73
75
|
|
74
76
|
def check_and_follow_redirect
|
75
|
-
if (@response.
|
76
|
-
|
77
|
-
url = uri.path
|
78
|
-
url << "?#{uri.query}" if uri.query.to_s.length > 0
|
79
|
-
|
80
|
-
args = {:host => uri.host}
|
81
|
-
args[:ssl] = true if uri.scheme == "https"
|
82
|
-
args[:port] = uri.port if uri.port
|
77
|
+
if (@response.code == "302" || @response.code == "307") && @response.header?("location") && @http2.args[:follow_redirects]
|
78
|
+
url, args = url_and_args_from_location
|
83
79
|
|
84
|
-
|
85
|
-
|
86
|
-
if !args[:host] or args[:host] == @args[:host]
|
87
|
-
return self.get(url)
|
80
|
+
if !args[:host] || args[:host] == @args[:host]
|
81
|
+
return @http2.get(url)
|
88
82
|
else
|
89
|
-
|
90
|
-
return http.get(url)
|
83
|
+
::Http2.new(args).get(url)
|
91
84
|
end
|
92
85
|
end
|
93
86
|
end
|
94
87
|
|
88
|
+
def url_and_args_from_location
|
89
|
+
uri = URI.parse(@response.header("location"))
|
90
|
+
url = uri.path
|
91
|
+
url << "?#{uri.query}" if uri.query.to_s.length > 0
|
92
|
+
|
93
|
+
args = {host: uri.host}
|
94
|
+
args[:ssl] = true if uri.scheme == "https"
|
95
|
+
args[:port] = uri.port if uri.port
|
96
|
+
|
97
|
+
return [url, args]
|
98
|
+
end
|
99
|
+
|
95
100
|
def check_and_decode
|
96
101
|
# Check if the content is gzip-encoded - if so: decode it!
|
97
102
|
if @encoding == "gzip"
|
98
103
|
puts "Http2: Decoding GZip." if @debug
|
99
104
|
require "zlib"
|
100
105
|
require "stringio"
|
101
|
-
io = StringIO.new(@response.
|
106
|
+
io = StringIO.new(@response.body)
|
102
107
|
gz = Zlib::GzipReader.new(io)
|
103
108
|
untrusted_str = gz.read
|
104
109
|
|
@@ -108,21 +113,21 @@ private
|
|
108
113
|
valid_string = untrusted_str.force_encoding("UTF-8").encode("UTF-8", :invalid => :replace, :replace => "").encode("UTF-8")
|
109
114
|
end
|
110
115
|
|
111
|
-
@response.
|
116
|
+
@response.body = valid_string
|
112
117
|
end
|
113
118
|
end
|
114
119
|
|
115
120
|
def handle_errors
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
121
|
+
return unless @http2.raise_errors
|
122
|
+
|
123
|
+
if @response.code == "500"
|
124
|
+
err = Http2::Errors::Internalserver.new("A internal server error occurred")
|
125
|
+
elsif @response.code == "403"
|
126
|
+
err = Http2::Errors::Noaccess.new("No access")
|
127
|
+
elsif @response.code == "400"
|
128
|
+
err = Http2::Errors::Badrequest.new("Bad request")
|
129
|
+
elsif @response.code == "404"
|
130
|
+
err = Http2::Errors::Notfound.new("Not found")
|
126
131
|
end
|
127
132
|
|
128
133
|
if err
|
@@ -135,8 +140,7 @@ private
|
|
135
140
|
if line
|
136
141
|
@rec_count += line.length
|
137
142
|
elsif !line && @rec_count <= 0
|
138
|
-
@
|
139
|
-
raise Errno::ECONNABORTED, "Server closed the connection before being able to read anything (KeepAliveMax: '#{@keepalive_max}', Connection: '#{@connection}', PID: '#{Process.pid}')."
|
143
|
+
raise Errno::ECONNABORTED, "Server closed the connection before being able to read anything (KeepAliveMax: '#{@http2.keepalive_max}', Connection: '#{@connection}', PID: '#{Process.pid}')."
|
140
144
|
end
|
141
145
|
end
|
142
146
|
|
@@ -147,26 +151,25 @@ private
|
|
147
151
|
end
|
148
152
|
|
149
153
|
def parse_keep_alive(keep_alive_line)
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
154
|
+
keep_alive_line.scan(/([a-z]+)=(\d+)/) do |match|
|
155
|
+
if match[0] == "timeout"
|
156
|
+
puts "Http2: Keepalive-max set to: '#{@keepalive_max}'." if @debug
|
157
|
+
@http2.keepalive_timeout = match[1].to_i
|
158
|
+
elsif match[0] == "max"
|
159
|
+
puts "Http2: Keepalive-timeout set to: '#{@keepalive_timeout}'." if @debug
|
160
|
+
@http2.keepalive_max = match[1].to_i
|
161
|
+
end
|
158
162
|
end
|
159
163
|
end
|
160
164
|
|
161
165
|
def parse_content_type(content_type_line)
|
162
166
|
if match_charset = content_type_line.match(/\s*;\s*charset=(.+)/i)
|
163
167
|
@charset = match_charset[1].downcase
|
164
|
-
@response.
|
168
|
+
@response.charset = @charset
|
165
169
|
content_type_line.gsub!(match_charset[0], "")
|
166
170
|
end
|
167
171
|
|
168
|
-
@
|
169
|
-
@response.args[:contenttype] = @content_type_line
|
172
|
+
@response.content_type = @content_type_line
|
170
173
|
end
|
171
174
|
|
172
175
|
#Parse a header-line and saves it on the object.
|
@@ -174,56 +177,57 @@ private
|
|
174
177
|
# http.parse_header("Content-Type: text/html\r\n")
|
175
178
|
def parse_header(line)
|
176
179
|
if match = line.match(/^(.+?):\s*(.+)#{@nl}$/)
|
177
|
-
key = match[1].
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
elsif key == "content-length"
|
189
|
-
@length = match[2].to_i
|
190
|
-
elsif key == "transfer-encoding"
|
191
|
-
@transfer_encoding = match[2].to_s.downcase.strip
|
192
|
-
end
|
180
|
+
key = match[1].downcase
|
181
|
+
set_header_special_values(key, match[2])
|
182
|
+
parse_normal_header(line, key, match[1], match[2])
|
183
|
+
elsif match = line.match(/^HTTP\/([\d\.]+)\s+(\d+)\s+(.+)$/)
|
184
|
+
@response.code = match[2]
|
185
|
+
@response.http_version = match[1]
|
186
|
+
@http2.on_content_call(@args, line)
|
187
|
+
else
|
188
|
+
raise "Could not understand header string: '#{line}'."
|
189
|
+
end
|
190
|
+
end
|
193
191
|
|
194
|
-
|
195
|
-
|
196
|
-
|
192
|
+
def set_header_special_values(key, value)
|
193
|
+
parse_cookie(value) if key == "set-cookie"
|
194
|
+
parse_keep_alive(value) if key == "keep-alive"
|
195
|
+
parse_content_type(value) if key == "content-type"
|
196
|
+
|
197
|
+
if key == "connection"
|
198
|
+
@connection = value.downcase
|
199
|
+
elsif key == "content-encoding"
|
200
|
+
@encoding = value.downcase
|
201
|
+
puts "Http2: Setting encoding to #{@encoding}" if @debug
|
202
|
+
elsif key == "content-length"
|
203
|
+
@length = value.to_i
|
204
|
+
elsif key == "transfer-encoding"
|
205
|
+
@transfer_encoding = value.downcase.strip
|
206
|
+
end
|
207
|
+
end
|
197
208
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
@response.args[:code] = match[2]
|
203
|
-
@response.args[:http_version] = match[1]
|
209
|
+
def parse_normal_header(line, key, orig_key, value)
|
210
|
+
puts "Http2: Parsed header: #{orig_key}: #{value}" if @debug
|
211
|
+
@response.headers[key] = [] unless @response.headers.key?(key)
|
212
|
+
@response.headers[key] << value
|
204
213
|
|
214
|
+
if key != "transfer-encoding" && key != "content-length" && key != "connection" && key != "keep-alive"
|
205
215
|
@http2.on_content_call(@args, line)
|
206
|
-
else
|
207
|
-
raise "Could not understand header string: '#{line}'.\n\n#{@sock.read(409600)}"
|
208
216
|
end
|
209
217
|
end
|
210
218
|
|
211
219
|
#Parses the body based on given headers and saves it to the result-object.
|
212
220
|
# http.parse_body(str)
|
213
221
|
def parse_body(line)
|
214
|
-
if @
|
215
|
-
return "break" if @length == 0
|
222
|
+
return :break if @length == 0
|
216
223
|
|
217
|
-
|
218
|
-
|
219
|
-
else
|
220
|
-
puts "Http2: Adding #{line.to_s.bytesize} to the body." if @debug
|
221
|
-
@response.args[:body] << line.to_s
|
222
|
-
@http2.on_content_call(@args, line)
|
223
|
-
return "break" if @response.header?("content-length") && @response.args[:body].length >= @response.header("content-length").to_i
|
224
|
-
end
|
224
|
+
if @transfer_encoding == "chunked"
|
225
|
+
return parse_body_chunked(line)
|
225
226
|
else
|
226
|
-
|
227
|
+
puts "Http2: Adding #{line.to_s.bytesize} to the body." if @debug
|
228
|
+
@response.body << line
|
229
|
+
@http2.on_content_call(@args, line)
|
230
|
+
return :break if @response.content_length && @response.body.length >= @response.content_length
|
227
231
|
end
|
228
232
|
end
|
229
233
|
|
@@ -231,16 +235,16 @@ private
|
|
231
235
|
len = line.strip.hex
|
232
236
|
|
233
237
|
if len > 0
|
234
|
-
read = @
|
235
|
-
return
|
236
|
-
@response.
|
238
|
+
read = @conn.read(len)
|
239
|
+
return :break if read == "" || read == "\n" || read == "\r\n"
|
240
|
+
@response.body << read
|
237
241
|
@http2.on_content_call(@args, read)
|
238
242
|
end
|
239
243
|
|
240
|
-
nl = @
|
244
|
+
nl = @conn.gets
|
241
245
|
if len == 0
|
242
246
|
if nl == "\n" || nl == "\r\n"
|
243
|
-
return
|
247
|
+
return :break
|
244
248
|
else
|
245
249
|
raise "Dont know what to do :'-("
|
246
250
|
end
|