rfuzz 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. data/Rakefile +1 -1
  2. data/doc/rdoc/classes/RFuzz/Browser.html +15 -15
  3. data/doc/rdoc/classes/RFuzz/Browser.src/{M000068.html → M000083.html} +0 -0
  4. data/doc/rdoc/classes/RFuzz/Browser.src/{M000069.html → M000084.html} +0 -0
  5. data/doc/rdoc/classes/RFuzz/Browser.src/{M000070.html → M000085.html} +0 -0
  6. data/doc/rdoc/classes/RFuzz/HttpClient.html +114 -57
  7. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000019.html +12 -13
  8. data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000011.html → M000020.html} +20 -20
  9. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000021.html +28 -0
  10. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000022.html +30 -0
  11. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000023.html +35 -0
  12. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000024.html +22 -0
  13. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000025.html +44 -0
  14. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000026.html +38 -0
  15. data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000016.html → M000027.html} +12 -12
  16. data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000017.html → M000028.html} +20 -20
  17. data/doc/rdoc/classes/RFuzz/HttpClient.src/{M000018.html → M000029.html} +4 -4
  18. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000030.html +26 -0
  19. data/doc/rdoc/classes/RFuzz/HttpClientError.html +118 -0
  20. data/doc/rdoc/classes/RFuzz/HttpEncoding.html +7 -4
  21. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000001.html +12 -12
  22. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000002.html +4 -4
  23. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000003.html +12 -12
  24. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000004.html +4 -4
  25. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000005.html +18 -18
  26. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000006.html +4 -4
  27. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000007.html +6 -6
  28. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000008.html +6 -6
  29. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000009.html +18 -18
  30. data/doc/rdoc/classes/RFuzz/HttpResponse.html +74 -13
  31. data/doc/rdoc/classes/RFuzz/HttpResponse.src/M000031.html +22 -0
  32. data/doc/rdoc/classes/RFuzz/HttpResponse.src/M000032.html +18 -0
  33. data/doc/rdoc/classes/RFuzz/HttpResponse.src/M000033.html +18 -0
  34. data/doc/rdoc/classes/RFuzz/Notifier.html +49 -31
  35. data/doc/rdoc/classes/RFuzz/Notifier.src/{M000044.html → M000058.html} +3 -3
  36. data/doc/rdoc/classes/RFuzz/Notifier.src/{M000045.html → M000059.html} +3 -3
  37. data/doc/rdoc/classes/RFuzz/Notifier.src/{M000047.html → M000060.html} +2 -2
  38. data/doc/rdoc/classes/RFuzz/Notifier.src/{M000048.html → M000061.html} +2 -2
  39. data/doc/rdoc/classes/RFuzz/Notifier.src/{M000049.html → M000062.html} +2 -2
  40. data/doc/rdoc/classes/RFuzz/Notifier.src/{M000046.html → M000063.html} +4 -4
  41. data/doc/rdoc/classes/RFuzz/Notifier.src/M000064.html +17 -0
  42. data/doc/rdoc/classes/RFuzz/PushBackIO.html +296 -0
  43. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000010.html +19 -0
  44. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000011.html +20 -0
  45. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000012.html +19 -0
  46. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000013.html +18 -0
  47. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000014.html +44 -0
  48. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000015.html +18 -0
  49. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000016.html +18 -0
  50. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000017.html +18 -0
  51. data/doc/rdoc/classes/RFuzz/PushBackIO.src/M000018.html +22 -0
  52. data/doc/rdoc/classes/RFuzz/RandomGenerator.html +62 -62
  53. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000032.html → M000046.html} +0 -0
  54. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000033.html → M000047.html} +0 -0
  55. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000036.html → M000050.html} +0 -0
  56. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000037.html → M000051.html} +0 -0
  57. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000038.html → M000052.html} +0 -0
  58. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000039.html → M000053.html} +0 -0
  59. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000040.html → M000054.html} +0 -0
  60. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000041.html → M000055.html} +0 -0
  61. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000042.html → M000056.html} +0 -0
  62. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/{M000043.html → M000057.html} +0 -0
  63. data/doc/rdoc/classes/RFuzz/Sampler.html +60 -60
  64. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000056.html → M000071.html} +0 -0
  65. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000057.html → M000072.html} +0 -0
  66. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000058.html → M000073.html} +0 -0
  67. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000059.html → M000074.html} +0 -0
  68. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000060.html → M000075.html} +0 -0
  69. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000061.html → M000076.html} +0 -0
  70. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000062.html → M000077.html} +0 -0
  71. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000063.html → M000078.html} +0 -0
  72. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000064.html → M000079.html} +0 -0
  73. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000065.html → M000080.html} +0 -0
  74. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000066.html → M000081.html} +0 -0
  75. data/doc/rdoc/classes/RFuzz/Sampler.src/{M000067.html → M000082.html} +0 -0
  76. data/doc/rdoc/classes/RFuzz/Session.html +63 -63
  77. data/doc/rdoc/classes/RFuzz/Session.src/{M000020.html → M000034.html} +0 -0
  78. data/doc/rdoc/classes/RFuzz/Session.src/{M000021.html → M000035.html} +0 -0
  79. data/doc/rdoc/classes/RFuzz/Session.src/{M000022.html → M000036.html} +0 -0
  80. data/doc/rdoc/classes/RFuzz/Session.src/{M000023.html → M000037.html} +0 -0
  81. data/doc/rdoc/classes/RFuzz/Session.src/{M000024.html → M000038.html} +0 -0
  82. data/doc/rdoc/classes/RFuzz/Session.src/{M000025.html → M000039.html} +0 -0
  83. data/doc/rdoc/classes/RFuzz/Session.src/{M000026.html → M000040.html} +0 -0
  84. data/doc/rdoc/classes/RFuzz/Session.src/{M000027.html → M000041.html} +0 -0
  85. data/doc/rdoc/classes/RFuzz/Session.src/{M000028.html → M000042.html} +0 -0
  86. data/doc/rdoc/classes/RFuzz/Session.src/{M000029.html → M000043.html} +0 -0
  87. data/doc/rdoc/classes/RFuzz/Session.src/{M000030.html → M000044.html} +0 -0
  88. data/doc/rdoc/classes/RFuzz/Session.src/{M000031.html → M000045.html} +0 -0
  89. data/doc/rdoc/classes/RFuzz/StatsTracker.html +32 -32
  90. data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000050.html → M000065.html} +0 -0
  91. data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000051.html → M000066.html} +0 -0
  92. data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000052.html → M000067.html} +0 -0
  93. data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000053.html → M000068.html} +0 -0
  94. data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000054.html → M000069.html} +0 -0
  95. data/doc/rdoc/classes/RFuzz/StatsTracker.src/{M000055.html → M000070.html} +0 -0
  96. data/doc/rdoc/classes/RFuzz.html +7 -1
  97. data/doc/rdoc/created.rid +1 -1
  98. data/doc/rdoc/files/lib/rfuzz/client_rb.html +2 -2
  99. data/doc/rdoc/files/lib/rfuzz/pushbackio_rb.html +108 -0
  100. data/doc/rdoc/fr_class_index.html +2 -0
  101. data/doc/rdoc/fr_file_index.html +1 -0
  102. data/doc/rdoc/fr_method_index.html +76 -61
  103. data/examples/cl_watcher.rb +12 -9
  104. data/examples/hpricot_pudding.rb +1 -1
  105. data/examples/mongrel_test_suite/test/http/protocol_parameters.rb +0 -3
  106. data/examples/mongrel_test_suite/test/rails/catastrophe.rb +26 -0
  107. data/examples/mongrel_test_suite/test/rails/conditional.rb +81 -0
  108. data/examples/mongrel_test_suite/test/rails/put.rb +25 -0
  109. data/examples/mongrel_test_suite/test/rails/redirect.rb +13 -0
  110. data/examples/mongrel_test_suite/test/rails/static_files.rb +47 -2
  111. data/examples/rails_security_test.rb +61 -0
  112. data/ext/http11_client/http11_client.c +15 -1
  113. data/ext/http11_client/http11_parser.c +627 -203
  114. data/ext/http11_client/http11_parser.h +2 -0
  115. data/ext/http11_client/http11_parser.rl +8 -4
  116. data/lib/rfuzz/client.rb +124 -115
  117. data/lib/rfuzz/pushbackio.rb +90 -0
  118. metadata +86 -60
  119. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000010.html +0 -24
  120. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000012.html +0 -50
  121. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000013.html +0 -49
  122. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000014.html +0 -57
  123. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000015.html +0 -37
@@ -32,6 +32,8 @@ typedef struct httpclient_parser {
32
32
  element_cb chunk_size;
33
33
  element_cb http_version;
34
34
  element_cb header_done;
35
+ element_cb last_chunk;
36
+
35
37
 
36
38
  } httpclient_parser;
37
39
 
@@ -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
- Chunked_Body = (chunk | last_chunk);
92
+ Chunked_Header = (chunk | last_chunk);
89
93
 
90
- main := Response | Chunked_Body;
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. Attached
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 += encode_field(COOKIE, encode_param(k,x)) }
67
+ v.each {|x| result << encode_field(COOKIE, encode_param(k,x)) }
52
68
  else
53
- result += encode_field(COOKIE, encode_param(k,v))
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
- "#{k}: #{v}\r\n"
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 += encode_field(k,x) }
86
+ v.each {|x| result << encode_field(k,x) }
71
87
  else
72
- result += encode_field(k,v)
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
- "#{host}" + (port.to_i != 80 ? ":#{port}" : "")
123
+ host + (port.to_i != 80 ? ":#{port}" : "")
108
124
  end
109
125
 
110
- # Performs URI escaping so that you can construct proper
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. (Stolen from Camping).
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("\r\n")
263
+ out.write(CRLF)
247
264
  ops[:body] || ""
248
265
  end
249
266
 
250
- def read_chunks(input, out, parser)
251
- begin
252
- until input.closed?
253
- parser.reset
254
- chunk = HttpResponse.new
255
- line = input.readline("\r\n")
256
- nread = parser.execute(chunk, line, 0)
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
- size = chunk.http_chunk_size ? chunk.http_chunk_size.to_i(base=16) : 0
276
+ while !@parser.finished?
277
+ data << @sock.read(CHUNK_SIZE, partial=true)
278
+ nread = @parser.execute(resp, data, nread)
279
+ end
264
280
 
265
- if size == 0
266
- return :finished, nil
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
- line = input.read(2)
273
- if line.nil? or line.length < 2
274
- return :incomplete_trailer, line
275
- elsif line != "\r\n"
276
- raise HttpClientParserError.new("invalid chunked encoding trailer")
277
- end
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
- def read_chunked_encoding(resp, sock, parser)
287
- out = StringIO.new
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
- case status
294
- when :incomplete_trailer
295
- if result.nil?
296
- sock.read(2)
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
- sock.read((result.length - 2).abs)
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
- # then continue reading them from the socket
315
- status, result = read_chunks(sock, out, parser)
324
+ header
325
+ end
316
326
 
317
- # and now the http_body is the chunk
318
- out.rewind; return out.read
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(sock)
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
- data = sock.readpartial(1024)
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[TRANSFER_ENCODING] and resp[TRANSFER_ENCODING].index("chunked")
345
- resp.http_body = read_chunked_encoding(resp, sock, parser)
352
+ if resp.chunked_encoding?
353
+ read_chunked_body(resp)
346
354
  elsif resp[CONTENT_LENGTH]
347
- cl = resp[CONTENT_LENGTH].to_i
348
- if cl - resp.http_body.length > 0
349
- resp.http_body += sock.read(cl - resp.http_body.length)
350
- elsif cl < resp.http_body.length
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
- resp.http_body += sock.read
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
- if resp[SET_COOKIE]
359
- cookies = query_parse(resp[SET_COOKIE], ';,')
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(sock)
390
+ return read_response
389
391
  rescue Object
390
392
  raise $!
391
393
  ensure
392
- sock.close unless (!sock or sock.closed?)
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