rfuzz 0.7 → 0.8
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.
- data/Rakefile +1 -1
- data/doc/rdoc/classes/RFuzz/Browser.html +15 -15
- data/doc/rdoc/classes/RFuzz/Browser.src/{M000068.html → M000083.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Browser.src/{M000069.html → M000084.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Browser.src/{M000070.html → M000085.html} +0 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.html +114 -57
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000019.html +12 -13
- data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000011.html → M000020.html} +20 -20
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000021.html +28 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000022.html +30 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000023.html +35 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000024.html +22 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000025.html +44 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000026.html +38 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000016.html → M000027.html} +12 -12
- data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000017.html → M000028.html} +20 -20
- data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000018.html → M000029.html} +4 -4
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000030.html +26 -0
- data/doc/rdoc/classes/RFuzz/HttpClientError.html +118 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.html +7 -4
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000001.html +12 -12
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000002.html +4 -4
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000003.html +12 -12
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000004.html +4 -4
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000005.html +18 -18
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000006.html +4 -4
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000007.html +6 -6
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000008.html +6 -6
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000009.html +18 -18
- data/doc/rdoc/classes/RFuzz/HttpResponse.html +74 -13
- data/doc/rdoc/classes/RFuzz/HttpResponse.src/M000031.html +22 -0
- data/doc/rdoc/classes/RFuzz/HttpResponse.src/M000032.html +18 -0
- data/doc/rdoc/classes/RFuzz/HttpResponse.src/M000033.html +18 -0
- data/doc/rdoc/classes/RFuzz/Notifier.html +49 -31
- data/doc/rdoc/classes/RFuzz/Notifier.src/{M000044.html → M000058.html} +3 -3
- data/doc/rdoc/classes/RFuzz/Notifier.src/{M000045.html → M000059.html} +3 -3
- data/doc/rdoc/classes/RFuzz/Notifier.src/{M000047.html → M000060.html} +2 -2
- data/doc/rdoc/classes/RFuzz/Notifier.src/{M000048.html → M000061.html} +2 -2
- data/doc/rdoc/classes/RFuzz/Notifier.src/{M000049.html → M000062.html} +2 -2
- data/doc/rdoc/classes/RFuzz/Notifier.src/{M000046.html → M000063.html} +4 -4
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000064.html +17 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.html +296 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000010.html +19 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000011.html +20 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000012.html +19 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000013.html +18 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000014.html +44 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000015.html +18 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000016.html +18 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000017.html +18 -0
- data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000018.html +22 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.html +62 -62
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000032.html → M000046.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000033.html → M000047.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000036.html → M000050.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000037.html → M000051.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000038.html → M000052.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000039.html → M000053.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000040.html → M000054.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000041.html → M000055.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000042.html → M000056.html} +0 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000043.html → M000057.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.html +60 -60
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000056.html → M000071.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000057.html → M000072.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000058.html → M000073.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000059.html → M000074.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000060.html → M000075.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000061.html → M000076.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000062.html → M000077.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000063.html → M000078.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000064.html → M000079.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000065.html → M000080.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000066.html → M000081.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/{M000067.html → M000082.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.html +63 -63
- data/doc/rdoc/classes/RFuzz/Session.src/{M000020.html → M000034.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000021.html → M000035.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000022.html → M000036.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000023.html → M000037.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000024.html → M000038.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000025.html → M000039.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000026.html → M000040.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000027.html → M000041.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000028.html → M000042.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000029.html → M000043.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000030.html → M000044.html} +0 -0
- data/doc/rdoc/classes/RFuzz/Session.src/{M000031.html → M000045.html} +0 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.html +32 -32
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000050.html → M000065.html} +0 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000051.html → M000066.html} +0 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000052.html → M000067.html} +0 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000053.html → M000068.html} +0 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000054.html → M000069.html} +0 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000055.html → M000070.html} +0 -0
- data/doc/rdoc/classes/RFuzz.html +7 -1
- data/doc/rdoc/created.rid +1 -1
- data/doc/rdoc/files/lib/rfuzz/client_rb.html +2 -2
- data/doc/rdoc/files/lib/rfuzz/pushbackio_rb.html +108 -0
- data/doc/rdoc/fr_class_index.html +2 -0
- data/doc/rdoc/fr_file_index.html +1 -0
- data/doc/rdoc/fr_method_index.html +76 -61
- data/examples/cl_watcher.rb +12 -9
- data/examples/hpricot_pudding.rb +1 -1
- data/examples/mongrel_test_suite/test/http/protocol_parameters.rb +0 -3
- data/examples/mongrel_test_suite/test/rails/catastrophe.rb +26 -0
- data/examples/mongrel_test_suite/test/rails/conditional.rb +81 -0
- data/examples/mongrel_test_suite/test/rails/put.rb +25 -0
- data/examples/mongrel_test_suite/test/rails/redirect.rb +13 -0
- data/examples/mongrel_test_suite/test/rails/static_files.rb +47 -2
- data/examples/rails_security_test.rb +61 -0
- data/ext/http11_client/http11_client.c +15 -1
- data/ext/http11_client/http11_parser.c +627 -203
- data/ext/http11_client/http11_parser.h +2 -0
- data/ext/http11_client/http11_parser.rl +8 -4
- data/lib/rfuzz/client.rb +124 -115
- data/lib/rfuzz/pushbackio.rb +90 -0
- metadata +86 -60
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000010.html +0 -24
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000012.html +0 -50
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000013.html +0 -49
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000014.html +0 -57
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000015.html +0 -37
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
parser->chunk_size(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
action last_chunk {
|
|
54
|
+
parser->last_chunk(parser->data, NULL, 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
action done {
|
|
54
58
|
parser->body_start = fpc - buffer + 1;
|
|
55
59
|
if(parser->header_done != NULL)
|
|
@@ -75,19 +79,19 @@
|
|
|
75
79
|
|
|
76
80
|
field_name = token+ >start_field %write_field;
|
|
77
81
|
field_value = any* >start_value %write_value;
|
|
78
|
-
message_header = field_name ": " field_value :> CRLF;
|
|
82
|
+
message_header = field_name ":" " "* field_value :> CRLF;
|
|
79
83
|
|
|
80
84
|
Response = Status_Line (message_header)* (CRLF @done);
|
|
81
85
|
|
|
82
86
|
chunk_ext_val = token+;
|
|
83
87
|
chunk_ext_name = token+;
|
|
84
88
|
chunk_extension = (";" chunk_ext_name >start_field %write_field %start_value ("=" chunk_ext_val >start_value)? %write_value )*;
|
|
85
|
-
last_chunk = "0"? chunk_extension :> (CRLF @done);
|
|
89
|
+
last_chunk = "0"? chunk_extension :> (CRLF @last_chunk @done);
|
|
86
90
|
chunk_size = xdigit+;
|
|
87
91
|
chunk = chunk_size >mark %chunk_size chunk_extension :> (CRLF @done);
|
|
88
|
-
|
|
92
|
+
Chunked_Header = (chunk | last_chunk);
|
|
89
93
|
|
|
90
|
-
main := Response |
|
|
94
|
+
main := Response | Chunked_Header;
|
|
91
95
|
}%%
|
|
92
96
|
|
|
93
97
|
/** Data **/
|
data/lib/rfuzz/client.rb
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
require 'http11_client'
|
|
2
2
|
require 'socket'
|
|
3
|
-
require 'stringio'
|
|
4
3
|
require 'rfuzz/stats'
|
|
5
4
|
require 'timeout'
|
|
5
|
+
require 'rfuzz/pushbackio'
|
|
6
6
|
|
|
7
7
|
module RFuzz
|
|
8
8
|
|
|
9
|
+
# Thrown for errors not related to the protocol format (HttpClientParserError are
|
|
10
|
+
# thrown for that).
|
|
11
|
+
class HttpClientError < StandardError; end
|
|
9
12
|
|
|
10
13
|
# A simple hash is returned for each request made by HttpClient with
|
|
11
|
-
# the headers that were given by the server for that request.
|
|
12
|
-
# to this are four attributes you can play with:
|
|
13
|
-
#
|
|
14
|
-
# * http_reason
|
|
15
|
-
# * http_version
|
|
16
|
-
# * http_status
|
|
17
|
-
# * http_body
|
|
18
|
-
#
|
|
19
|
-
# These are set internally by the Ragel/C parser so they're very fast
|
|
20
|
-
# and pretty much C voodoo. You can modify them without fear once you get
|
|
21
|
-
# the response.
|
|
14
|
+
# the headers that were given by the server for that request.
|
|
22
15
|
class HttpResponse < Hash
|
|
23
16
|
# The reason returned in the http response ("OK","File not found",etc.)
|
|
24
17
|
attr_accessor :http_reason
|
|
@@ -34,6 +27,28 @@ module RFuzz
|
|
|
34
27
|
|
|
35
28
|
# When parsing chunked encodings this is set
|
|
36
29
|
attr_accessor :http_chunk_size
|
|
30
|
+
|
|
31
|
+
# The actual chunks taken from the chunked encoding
|
|
32
|
+
attr_accessor :raw_chunks
|
|
33
|
+
|
|
34
|
+
# Converts the http_chunk_size string properly
|
|
35
|
+
def chunk_size
|
|
36
|
+
if @chunk_size == nil
|
|
37
|
+
@chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@chunk_size
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# true if this is the last chunk, nil otherwise (false)
|
|
44
|
+
def last_chunk?
|
|
45
|
+
@last_chunk || chunk_size == 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Easier way to find out if this is a chunked encoding
|
|
49
|
+
def chunked_encoding?
|
|
50
|
+
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
|
51
|
+
end
|
|
37
52
|
end
|
|
38
53
|
|
|
39
54
|
# A mixin that has most of the HTTP encoding methods you need to work
|
|
@@ -41,6 +56,7 @@ module RFuzz
|
|
|
41
56
|
# as well.
|
|
42
57
|
module HttpEncoding
|
|
43
58
|
COOKIE="Cookie"
|
|
59
|
+
FIELD_ENCODING="%s: %s\r\n"
|
|
44
60
|
|
|
45
61
|
# Converts a Hash of cookies to the appropriate simple cookie
|
|
46
62
|
# headers.
|
|
@@ -48,9 +64,9 @@ module RFuzz
|
|
|
48
64
|
result = ""
|
|
49
65
|
cookies.each do |k,v|
|
|
50
66
|
if v.kind_of? Array
|
|
51
|
-
v.each {|x| result
|
|
67
|
+
v.each {|x| result << encode_field(COOKIE, encode_param(k,x)) }
|
|
52
68
|
else
|
|
53
|
-
result
|
|
69
|
+
result << encode_field(COOKIE, encode_param(k,v))
|
|
54
70
|
end
|
|
55
71
|
end
|
|
56
72
|
return result
|
|
@@ -58,7 +74,7 @@ module RFuzz
|
|
|
58
74
|
|
|
59
75
|
# Encode HTTP header fields of "k: v\r\n"
|
|
60
76
|
def encode_field(k,v)
|
|
61
|
-
|
|
77
|
+
FIELD_ENCODING % [k,v]
|
|
62
78
|
end
|
|
63
79
|
|
|
64
80
|
# Encodes the headers given in the hash returning a string
|
|
@@ -67,9 +83,9 @@ module RFuzz
|
|
|
67
83
|
result = ""
|
|
68
84
|
head.each do |k,v|
|
|
69
85
|
if v.kind_of? Array
|
|
70
|
-
v.each {|x| result
|
|
86
|
+
v.each {|x| result << encode_field(k,x) }
|
|
71
87
|
else
|
|
72
|
-
result
|
|
88
|
+
result << encode_field(k,v)
|
|
73
89
|
end
|
|
74
90
|
end
|
|
75
91
|
return result
|
|
@@ -104,12 +120,10 @@ module RFuzz
|
|
|
104
120
|
# a Host header, but if you include port 80 then further
|
|
105
121
|
# redirects will tack on the :80 which is annoying.
|
|
106
122
|
def encode_host(host, port)
|
|
107
|
-
|
|
123
|
+
host + (port.to_i != 80 ? ":#{port}" : "")
|
|
108
124
|
end
|
|
109
125
|
|
|
110
|
-
#
|
|
111
|
-
# query strings faster. Use this rather than the cgi.rb
|
|
112
|
-
# version since it's faster. (Stolen from Camping).
|
|
126
|
+
# Escapes a URI.
|
|
113
127
|
def escape(s)
|
|
114
128
|
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
|
115
129
|
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
|
@@ -117,7 +131,7 @@ module RFuzz
|
|
|
117
131
|
end
|
|
118
132
|
|
|
119
133
|
|
|
120
|
-
# Unescapes a URI escaped string.
|
|
134
|
+
# Unescapes a URI escaped string.
|
|
121
135
|
def unescape(s)
|
|
122
136
|
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
|
123
137
|
[$1.delete('%')].pack('H*')
|
|
@@ -206,6 +220,8 @@ module RFuzz
|
|
|
206
220
|
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
|
207
221
|
REQ_CONTENT_LENGTH="Content-Length"
|
|
208
222
|
REQ_HOST="Host"
|
|
223
|
+
CHUNK_SIZE=1024 * 16
|
|
224
|
+
CRLF="\r\n"
|
|
209
225
|
|
|
210
226
|
# Access to the host, port, default options, and cookies currently in play
|
|
211
227
|
attr_accessor :host, :port, :options, :cookies, :allowed_methods, :notifier
|
|
@@ -219,6 +235,7 @@ module RFuzz
|
|
|
219
235
|
@allowed_methods = options[:allowed_methods] || [:put, :get, :post, :delete, :head]
|
|
220
236
|
@notifier = options[:notifier]
|
|
221
237
|
@redirect = options[:redirect] || false
|
|
238
|
+
@parser = HttpClientParser.new
|
|
222
239
|
end
|
|
223
240
|
|
|
224
241
|
|
|
@@ -243,79 +260,78 @@ module RFuzz
|
|
|
243
260
|
out.write(HTTP_REQUEST_HEADER % [method, encode_query(uri,query)])
|
|
244
261
|
out.write(encode_headers(head))
|
|
245
262
|
out.write(encode_cookies(@cookies.merge(req[:cookies] || {})))
|
|
246
|
-
out.write(
|
|
263
|
+
out.write(CRLF)
|
|
247
264
|
ops[:body] || ""
|
|
248
265
|
end
|
|
249
266
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if !parser.finished?
|
|
259
|
-
# tried to read this header but couldn't
|
|
260
|
-
return :incomplete_header, line
|
|
261
|
-
end
|
|
267
|
+
# Does the read operations needed to parse a header with the @parser.
|
|
268
|
+
# A "header" in this case is either an HTTP header or a Chunked encoding
|
|
269
|
+
# header (since the @parser handles both).
|
|
270
|
+
def read_parsed_header
|
|
271
|
+
@parser.reset
|
|
272
|
+
resp = HttpResponse.new
|
|
273
|
+
data = @sock.read(CHUNK_SIZE, partial=true)
|
|
274
|
+
nread = @parser.execute(resp, data, 0)
|
|
262
275
|
|
|
263
|
-
|
|
276
|
+
while !@parser.finished?
|
|
277
|
+
data << @sock.read(CHUNK_SIZE, partial=true)
|
|
278
|
+
nread = @parser.execute(resp, data, nread)
|
|
279
|
+
end
|
|
264
280
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
end
|
|
281
|
+
return resp
|
|
282
|
+
end
|
|
268
283
|
|
|
269
|
-
remain = size - out.write(input.read(size))
|
|
270
|
-
return :incomplete_body, remain if remain > 0
|
|
271
284
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
285
|
+
# Used to process chunked headers and then read up their bodies.
|
|
286
|
+
def read_chunked_header
|
|
287
|
+
resp = read_parsed_header
|
|
288
|
+
@sock.push(resp.http_body)
|
|
289
|
+
|
|
290
|
+
if !resp.last_chunk?
|
|
291
|
+
resp.http_body = @sock.read(resp.chunk_size)
|
|
292
|
+
|
|
293
|
+
trail = @sock.read(2)
|
|
294
|
+
if trail != CRLF
|
|
295
|
+
raise HttpClientParserError.new("Chunk ended in #{trail.inspect} not #{CRLF.inspect}")
|
|
278
296
|
end
|
|
279
|
-
rescue EOFError
|
|
280
|
-
# this is thrown when the header read is attempted and
|
|
281
|
-
# there's nothing in the buffer
|
|
282
|
-
return :eof_error, nil
|
|
283
297
|
end
|
|
284
|
-
end
|
|
285
298
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
input = StringIO.new(resp.http_body)
|
|
299
|
+
return resp
|
|
300
|
+
end
|
|
289
301
|
|
|
290
|
-
# read from the http body first, then continue at the socket
|
|
291
|
-
status, result = read_chunks(input, out, parser)
|
|
292
302
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
# Collects up a chunked body both collecting the body together *and*
|
|
304
|
+
# collecting the chunks into HttpResponse.raw_chunks[] for alternative
|
|
305
|
+
# analysis.
|
|
306
|
+
def read_chunked_body(header)
|
|
307
|
+
@sock.push(header.http_body)
|
|
308
|
+
header.http_body = ""
|
|
309
|
+
header.raw_chunks = []
|
|
310
|
+
|
|
311
|
+
while true
|
|
312
|
+
@notifier.read_chunk(:begins) if @notifier
|
|
313
|
+
chunk = read_chunked_header
|
|
314
|
+
header.raw_chunks << chunk
|
|
315
|
+
if !chunk.last_chunk?
|
|
316
|
+
header.http_body << chunk.http_body
|
|
317
|
+
@notifier.read_chunk(:end) if @notifier
|
|
297
318
|
else
|
|
298
|
-
|
|
319
|
+
@notifier.read_chunk(:end) if @notifier
|
|
320
|
+
break # last chunk, done
|
|
299
321
|
end
|
|
300
|
-
when :incomplete_body
|
|
301
|
-
out.write(sock.read(result)) # read the remaining
|
|
302
|
-
sock.read(2)
|
|
303
|
-
when :incomplete_header
|
|
304
|
-
# push what we read back onto the socket, but backwards
|
|
305
|
-
result.reverse!
|
|
306
|
-
result.each_byte {|b| sock.ungetc(b) }
|
|
307
|
-
when :finished
|
|
308
|
-
# all done, get out
|
|
309
|
-
out.rewind; return out.read
|
|
310
|
-
when :eof_error
|
|
311
|
-
# read everything we could, ignore
|
|
312
322
|
end
|
|
313
323
|
|
|
314
|
-
|
|
315
|
-
|
|
324
|
+
header
|
|
325
|
+
end
|
|
316
326
|
|
|
317
|
-
|
|
318
|
-
|
|
327
|
+
# Reads the SET_COOKIE string out of resp and translates it into
|
|
328
|
+
# the @cookies store for this HttpClient.
|
|
329
|
+
def store_cookies(resp)
|
|
330
|
+
if resp[SET_COOKIE]
|
|
331
|
+
cookies = query_parse(resp[SET_COOKIE], ';')
|
|
332
|
+
@cookies.merge! cookies
|
|
333
|
+
@cookies.delete "path"
|
|
334
|
+
end
|
|
319
335
|
end
|
|
320
336
|
|
|
321
337
|
# Reads an HTTP response from the given socket. It uses
|
|
@@ -325,71 +341,59 @@ module RFuzz
|
|
|
325
341
|
# As with other methods in this class it doesn't stop any exceptions
|
|
326
342
|
# from reaching your code. It's for experts who want these exceptions
|
|
327
343
|
# so either write a wrapper, use net/http, or deal with it on your end.
|
|
328
|
-
def read_response
|
|
329
|
-
data, resp = nil, nil
|
|
330
|
-
parser = HttpClientParser.new
|
|
344
|
+
def read_response
|
|
331
345
|
resp = HttpResponse.new
|
|
332
346
|
|
|
333
347
|
notify :read_header do
|
|
334
|
-
|
|
335
|
-
nread = parser.execute(resp, data, 0)
|
|
336
|
-
|
|
337
|
-
while not parser.finished?
|
|
338
|
-
data += sock.readpartial(1024)
|
|
339
|
-
nread += parser.execute(resp, data, nread)
|
|
340
|
-
end
|
|
348
|
+
resp = read_parsed_header
|
|
341
349
|
end
|
|
342
350
|
|
|
343
351
|
notify :read_body do
|
|
344
|
-
if resp
|
|
345
|
-
|
|
352
|
+
if resp.chunked_encoding?
|
|
353
|
+
read_chunked_body(resp)
|
|
346
354
|
elsif resp[CONTENT_LENGTH]
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
STDERR.puts "Web site sucks, they said Content-Length: #{cl}, but sent a longer body length: #{resp.http_body.length}"
|
|
352
|
-
end
|
|
355
|
+
needs = resp[CONTENT_LENGTH].to_i - resp.http_body.length
|
|
356
|
+
# Some requests can actually give a content length, and then not have content
|
|
357
|
+
# so we ignore HttpClientError exceptions and pray that's good enough
|
|
358
|
+
resp.http_body += @sock.read(needs) if needs > 0 rescue HttpClientError
|
|
353
359
|
else
|
|
354
|
-
|
|
360
|
+
while true
|
|
361
|
+
begin
|
|
362
|
+
resp.http_body += @sock.read(CHUNK_SIZE, partial=true)
|
|
363
|
+
rescue HttpClientError
|
|
364
|
+
break # this is fine, they closed the socket then
|
|
365
|
+
end
|
|
366
|
+
end
|
|
355
367
|
end
|
|
356
368
|
end
|
|
357
369
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
@cookies.merge! cookies
|
|
361
|
-
@cookies.delete "path"
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
notify :close do
|
|
365
|
-
sock.close
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
resp
|
|
370
|
+
store_cookies(resp)
|
|
371
|
+
return resp
|
|
369
372
|
end
|
|
370
373
|
|
|
371
374
|
# Does the socket connect and then build_request, read_response
|
|
372
375
|
# calls finally returning the result.
|
|
373
376
|
def send_request(method, uri, req)
|
|
374
377
|
begin
|
|
375
|
-
sock = nil
|
|
376
378
|
notify :connect do
|
|
377
|
-
sock = TCPSocket.new(@host, @port)
|
|
379
|
+
@sock = PushBackIO.new(TCPSocket.new(@host, @port))
|
|
378
380
|
end
|
|
379
381
|
|
|
380
382
|
out = StringIO.new
|
|
381
383
|
body = build_request(out, method, uri, req)
|
|
382
384
|
|
|
383
385
|
notify :send_request do
|
|
384
|
-
sock.write(out.string + body)
|
|
385
|
-
sock.flush
|
|
386
|
+
@sock.write(out.string + body)
|
|
387
|
+
@sock.flush
|
|
386
388
|
end
|
|
387
389
|
|
|
388
|
-
return read_response
|
|
390
|
+
return read_response
|
|
389
391
|
rescue Object
|
|
390
392
|
raise $!
|
|
391
393
|
ensure
|
|
392
|
-
|
|
394
|
+
if @sock
|
|
395
|
+
notify(:close) { @sock.close }
|
|
396
|
+
end
|
|
393
397
|
end
|
|
394
398
|
end
|
|
395
399
|
|
|
@@ -406,7 +410,7 @@ module RFuzz
|
|
|
406
410
|
|
|
407
411
|
return resp
|
|
408
412
|
else
|
|
409
|
-
raise "Invalid method: #{symbol}"
|
|
413
|
+
raise HttpClientError.new("Invalid method: #{symbol}")
|
|
410
414
|
end
|
|
411
415
|
end
|
|
412
416
|
|
|
@@ -499,5 +503,10 @@ module RFuzz
|
|
|
499
503
|
# Before and after the client closes with the server.
|
|
500
504
|
def close(state)
|
|
501
505
|
end
|
|
506
|
+
|
|
507
|
+
# Called when a chunk from a chunked encoding is read.
|
|
508
|
+
def read_chunk(state)
|
|
509
|
+
end
|
|
502
510
|
end
|
|
511
|
+
|
|
503
512
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
3
|
+
module RFuzz
|
|
4
|
+
# A simple class that using a StringIO object internally to allow for faster
|
|
5
|
+
# and simpler "push back" semantics. It basically lets you read a random
|
|
6
|
+
# amount from a secondary IO object, parse what is needed, and then anything
|
|
7
|
+
# remaining can be quickly pushed back in one chunk for the next read.
|
|
8
|
+
class PushBackIO
|
|
9
|
+
attr_accessor :secondary
|
|
10
|
+
|
|
11
|
+
def initialize(secondary)
|
|
12
|
+
@secondary = secondary
|
|
13
|
+
@buffer = StringIO.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Pushes the given string content back onto the stream for the
|
|
17
|
+
# next read to handle.
|
|
18
|
+
def push(content)
|
|
19
|
+
if content.length > 0
|
|
20
|
+
@buffer.write(content)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pop(n)
|
|
25
|
+
@buffer.rewind
|
|
26
|
+
@buffer.read(n) || ""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset
|
|
30
|
+
@buffer.string = @buffer.read # reset out internal buffer
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# First does a read from the internal buffer, and then appends anything
|
|
34
|
+
# needed from the secondary IO to complete the request. The return
|
|
35
|
+
# value is guaranteed to be a String, and never nil. If it returns
|
|
36
|
+
# a string of length 0 then there is nothing to read from the buffer (most
|
|
37
|
+
# likely closed). It will also avoid reading from a secondary that's closed.
|
|
38
|
+
#
|
|
39
|
+
# If partial==true then readpartial is used instead.
|
|
40
|
+
def read(n, partial=false)
|
|
41
|
+
r = pop(n)
|
|
42
|
+
needs = n - r.length
|
|
43
|
+
|
|
44
|
+
if needs > 0
|
|
45
|
+
sec = ""
|
|
46
|
+
if partial
|
|
47
|
+
begin
|
|
48
|
+
protect do
|
|
49
|
+
sec = @secondary.readpartial(needs)
|
|
50
|
+
end
|
|
51
|
+
rescue EOFError
|
|
52
|
+
# TODO: notify closed? error?
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
protect { sec = @secondary.read(needs)}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
r << (sec || "")
|
|
59
|
+
|
|
60
|
+
# finally, if there's nothing at all returned then this is bad
|
|
61
|
+
if r.length == 0
|
|
62
|
+
raise HttpClientError.new("Server returned empty response.")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
reset
|
|
67
|
+
return r
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def flush
|
|
71
|
+
protect { @secondary.flush }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def write(content)
|
|
75
|
+
protect { @secondary.write(content) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def close
|
|
79
|
+
protect { @secondary.close }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def protect
|
|
83
|
+
if !@secondary.closed?
|
|
84
|
+
yield
|
|
85
|
+
else
|
|
86
|
+
raise HttpClientError.new("Socket closed.")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|