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.
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