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