rfuzz 0.6

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 (136) hide show
  1. data/COPYING +55 -0
  2. data/LICENSE +55 -0
  3. data/README +252 -0
  4. data/Rakefile +48 -0
  5. data/doc/rdoc/classes/RFuzz.html +146 -0
  6. data/doc/rdoc/classes/RFuzz/HttpClient.html +481 -0
  7. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000010.html +24 -0
  8. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000011.html +34 -0
  9. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000012.html +49 -0
  10. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000013.html +49 -0
  11. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000014.html +57 -0
  12. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000015.html +37 -0
  13. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000016.html +26 -0
  14. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000017.html +34 -0
  15. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000018.html +18 -0
  16. data/doc/rdoc/classes/RFuzz/HttpClient.src/M000019.html +26 -0
  17. data/doc/rdoc/classes/RFuzz/HttpEncoding.html +294 -0
  18. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000001.html +26 -0
  19. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000002.html +18 -0
  20. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000003.html +26 -0
  21. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000004.html +18 -0
  22. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000005.html +32 -0
  23. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000006.html +18 -0
  24. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000007.html +20 -0
  25. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000008.html +20 -0
  26. data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000009.html +32 -0
  27. data/doc/rdoc/classes/RFuzz/HttpResponse.html +180 -0
  28. data/doc/rdoc/classes/RFuzz/Notifier.html +252 -0
  29. data/doc/rdoc/classes/RFuzz/Notifier.src/M000044.html +17 -0
  30. data/doc/rdoc/classes/RFuzz/Notifier.src/M000045.html +17 -0
  31. data/doc/rdoc/classes/RFuzz/Notifier.src/M000046.html +17 -0
  32. data/doc/rdoc/classes/RFuzz/Notifier.src/M000047.html +17 -0
  33. data/doc/rdoc/classes/RFuzz/Notifier.src/M000048.html +17 -0
  34. data/doc/rdoc/classes/RFuzz/Notifier.src/M000049.html +17 -0
  35. data/doc/rdoc/classes/RFuzz/RandomGenerator.html +362 -0
  36. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000032.html +21 -0
  37. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000033.html +23 -0
  38. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000036.html +22 -0
  39. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000037.html +20 -0
  40. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000038.html +22 -0
  41. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000039.html +20 -0
  42. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000040.html +18 -0
  43. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000041.html +18 -0
  44. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000042.html +22 -0
  45. data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000043.html +18 -0
  46. data/doc/rdoc/classes/RFuzz/Sampler.html +383 -0
  47. data/doc/rdoc/classes/RFuzz/Sampler.src/M000056.html +19 -0
  48. data/doc/rdoc/classes/RFuzz/Sampler.src/M000057.html +23 -0
  49. data/doc/rdoc/classes/RFuzz/Sampler.src/M000058.html +26 -0
  50. data/doc/rdoc/classes/RFuzz/Sampler.src/M000059.html +18 -0
  51. data/doc/rdoc/classes/RFuzz/Sampler.src/M000060.html +18 -0
  52. data/doc/rdoc/classes/RFuzz/Sampler.src/M000061.html +18 -0
  53. data/doc/rdoc/classes/RFuzz/Sampler.src/M000062.html +18 -0
  54. data/doc/rdoc/classes/RFuzz/Sampler.src/M000063.html +19 -0
  55. data/doc/rdoc/classes/RFuzz/Sampler.src/M000064.html +18 -0
  56. data/doc/rdoc/classes/RFuzz/Sampler.src/M000065.html +23 -0
  57. data/doc/rdoc/classes/RFuzz/Sampler.src/M000066.html +18 -0
  58. data/doc/rdoc/classes/RFuzz/Sampler.src/M000067.html +20 -0
  59. data/doc/rdoc/classes/RFuzz/Session.html +415 -0
  60. data/doc/rdoc/classes/RFuzz/Session.src/M000020.html +31 -0
  61. data/doc/rdoc/classes/RFuzz/Session.src/M000021.html +18 -0
  62. data/doc/rdoc/classes/RFuzz/Session.src/M000022.html +18 -0
  63. data/doc/rdoc/classes/RFuzz/Session.src/M000023.html +34 -0
  64. data/doc/rdoc/classes/RFuzz/Session.src/M000024.html +19 -0
  65. data/doc/rdoc/classes/RFuzz/Session.src/M000025.html +19 -0
  66. data/doc/rdoc/classes/RFuzz/Session.src/M000026.html +26 -0
  67. data/doc/rdoc/classes/RFuzz/Session.src/M000027.html +29 -0
  68. data/doc/rdoc/classes/RFuzz/Session.src/M000028.html +19 -0
  69. data/doc/rdoc/classes/RFuzz/Session.src/M000029.html +18 -0
  70. data/doc/rdoc/classes/RFuzz/Session.src/M000030.html +18 -0
  71. data/doc/rdoc/classes/RFuzz/Session.src/M000031.html +23 -0
  72. data/doc/rdoc/classes/RFuzz/StatsTracker.html +242 -0
  73. data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000050.html +19 -0
  74. data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000051.html +19 -0
  75. data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000052.html +18 -0
  76. data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000053.html +18 -0
  77. data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000054.html +28 -0
  78. data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000055.html +18 -0
  79. data/doc/rdoc/created.rid +1 -0
  80. data/doc/rdoc/files/COPYING.html +168 -0
  81. data/doc/rdoc/files/LICENSE.html +168 -0
  82. data/doc/rdoc/files/README.html +473 -0
  83. data/doc/rdoc/files/lib/rfuzz/client_rb.html +111 -0
  84. data/doc/rdoc/files/lib/rfuzz/random_rb.html +116 -0
  85. data/doc/rdoc/files/lib/rfuzz/rfuzz_rb.html +109 -0
  86. data/doc/rdoc/files/lib/rfuzz/session_rb.html +111 -0
  87. data/doc/rdoc/files/lib/rfuzz/stats_rb.html +113 -0
  88. data/doc/rdoc/fr_class_index.html +35 -0
  89. data/doc/rdoc/fr_file_index.html +34 -0
  90. data/doc/rdoc/fr_method_index.html +93 -0
  91. data/doc/rdoc/index.html +24 -0
  92. data/doc/rdoc/rdoc-style.css +208 -0
  93. data/examples/amazon_headers.rb +38 -0
  94. data/examples/hpricot_pudding.rb +22 -0
  95. data/examples/kill_routes.rb +26 -0
  96. data/examples/mongrel_test_suite/lib/gen.rb +24 -0
  97. data/examples/mongrel_test_suite/test/camping/static_files.rb +9 -0
  98. data/examples/mongrel_test_suite/test/camping/upload_file.rb +9 -0
  99. data/examples/mongrel_test_suite/test/camping/upload_progress.rb +9 -0
  100. data/examples/mongrel_test_suite/test/http/base_protocol.rb +23 -0
  101. data/examples/mongrel_test_suite/test/nitro/upload_file.rb +9 -0
  102. data/examples/mongrel_test_suite/test/nitro/upload_progress.rb +9 -0
  103. data/examples/mongrel_test_suite/test/rails/static_files.rb +9 -0
  104. data/examples/mongrel_test_suite/test/rails/upload_file.rb +9 -0
  105. data/examples/mongrel_test_suite/test/rails/upload_progress.rb +9 -0
  106. data/examples/perftest.rb +30 -0
  107. data/ext/fuzzrnd/ext_help.h +14 -0
  108. data/ext/fuzzrnd/extconf.rb +6 -0
  109. data/ext/fuzzrnd/fuzzrnd.c +149 -0
  110. data/ext/http11_client/ext_help.h +14 -0
  111. data/ext/http11_client/extconf.rb +6 -0
  112. data/ext/http11_client/http11_client.c +288 -0
  113. data/ext/http11_client/http11_parser.c +629 -0
  114. data/ext/http11_client/http11_parser.h +46 -0
  115. data/ext/http11_client/http11_parser.rl +169 -0
  116. data/lib/rfuzz/client.rb +498 -0
  117. data/lib/rfuzz/random.rb +110 -0
  118. data/lib/rfuzz/rfuzz.rb +12 -0
  119. data/lib/rfuzz/session.rb +154 -0
  120. data/lib/rfuzz/stats.rb +159 -0
  121. data/resources/defaults.yaml +2 -0
  122. data/resources/words.txt +3310 -0
  123. data/test/coverage/index.html +388 -0
  124. data/test/coverage/lib-rfuzz-client_rb.html +1127 -0
  125. data/test/coverage/lib-rfuzz-random_rb.html +739 -0
  126. data/test/coverage/lib-rfuzz-session_rb.html +783 -0
  127. data/test/coverage/lib-rfuzz-stats_rb.html +788 -0
  128. data/test/server.rb +101 -0
  129. data/test/test_client.rb +164 -0
  130. data/test/test_fuzzrnd.rb +31 -0
  131. data/test/test_httpparser.rb +48 -0
  132. data/test/test_random.rb +75 -0
  133. data/test/test_session.rb +33 -0
  134. data/test/test_stats.rb +45 -0
  135. data/tools/rakehelp.rb +119 -0
  136. metadata +201 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright (c) 2005 Zed A. Shaw
3
+ * You can redistribute it and/or modify it under the same terms as Ruby.
4
+ */
5
+
6
+ #ifndef http11_parser_h
7
+ #define http11_parser_h
8
+
9
+ #include <sys/types.h>
10
+
11
+ #if defined(_WIN32)
12
+ #include <stddef.h>
13
+ #endif
14
+
15
+ typedef void (*element_cb)(void *data, const char *at, size_t length);
16
+ typedef void (*field_cb)(void *data, const char *field, size_t flen, const char *value, size_t vlen);
17
+
18
+ typedef struct httpclient_parser {
19
+ int cs;
20
+ size_t body_start;
21
+ int content_len;
22
+ size_t nread;
23
+ size_t mark;
24
+ size_t field_start;
25
+ size_t field_len;
26
+
27
+ void *data;
28
+
29
+ field_cb http_field;
30
+ element_cb reason_phrase;
31
+ element_cb status_code;
32
+ element_cb chunk_size;
33
+ element_cb http_version;
34
+ element_cb header_done;
35
+
36
+ } httpclient_parser;
37
+
38
+ int httpclient_parser_init(httpclient_parser *parser);
39
+ int httpclient_parser_finish(httpclient_parser *parser);
40
+ size_t httpclient_parser_execute(httpclient_parser *parser, const char *data, size_t len, size_t off);
41
+ int httpclient_parser_has_error(httpclient_parser *parser);
42
+ int httpclient_parser_is_finished(httpclient_parser *parser);
43
+
44
+ #define httpclient_parser_nread(parser) (parser)->nread
45
+
46
+ #endif
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Copyright (c) 2005 Zed A. Shaw
3
+ * You can redistribute it and/or modify it under the same terms as Ruby.
4
+ */
5
+
6
+ #include "http11_parser.h"
7
+ #include <stdio.h>
8
+ #include <assert.h>
9
+ #include <stdlib.h>
10
+ #include <ctype.h>
11
+ #include <string.h>
12
+
13
+ #define LEN(AT, FPC) (FPC - buffer - parser->AT)
14
+ #define MARK(M,FPC) (parser->M = (FPC) - buffer)
15
+ #define PTR_TO(F) (buffer + parser->F)
16
+ #define L(M) fprintf(stderr, "" # M "\n");
17
+
18
+
19
+ /** machine **/
20
+ %%{
21
+ machine httpclient_parser;
22
+
23
+ action mark {MARK(mark, fpc); }
24
+
25
+ action start_field { MARK(field_start, fpc); }
26
+
27
+ action write_field {
28
+ parser->field_len = LEN(field_start, fpc);
29
+ }
30
+
31
+ action start_value { MARK(mark, fpc); }
32
+
33
+ action write_value {
34
+ parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc));
35
+ }
36
+
37
+ action reason_phrase {
38
+ parser->reason_phrase(parser->data, PTR_TO(mark), LEN(mark, fpc));
39
+ }
40
+
41
+ action status_code {
42
+ parser->status_code(parser->data, PTR_TO(mark), LEN(mark, fpc));
43
+ }
44
+
45
+ action http_version {
46
+ parser->http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
47
+ }
48
+
49
+ action chunk_size {
50
+ parser->chunk_size(parser->data, PTR_TO(mark), LEN(mark, fpc));
51
+ }
52
+
53
+ action done {
54
+ parser->body_start = fpc - buffer + 1;
55
+ if(parser->header_done != NULL)
56
+ parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
57
+ fbreak;
58
+ }
59
+
60
+ # line endings
61
+ CRLF = "\r\n";
62
+
63
+ # character types
64
+ CTL = (cntrl | 127);
65
+ tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
66
+
67
+ # elements
68
+ token = (ascii -- (CTL | tspecials));
69
+
70
+ Reason_Phrase = (any -- CRLF)+ >mark %reason_phrase;
71
+ Status_Code = digit+ >mark %status_code;
72
+ http_number = (digit+ "." digit+) ;
73
+ HTTP_Version = ("HTTP/" http_number) >mark %http_version ;
74
+ Status_Line = HTTP_Version " " Status_Code " " Reason_Phrase :> CRLF;
75
+
76
+ field_name = token+ >start_field %write_field;
77
+ field_value = any* >start_value %write_value;
78
+ message_header = field_name ": " field_value :> CRLF;
79
+
80
+ Response = Status_Line (message_header)* (CRLF @done);
81
+
82
+ chunk_ext_val = token+;
83
+ chunk_ext_name = token+;
84
+ 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);
86
+ chunk_size = xdigit+;
87
+ chunk = chunk_size >mark %chunk_size chunk_extension :> (CRLF @done);
88
+ Chunked_Body = (chunk | last_chunk);
89
+
90
+ main := Response | Chunked_Body;
91
+ }%%
92
+
93
+ /** Data **/
94
+ %% write data;
95
+
96
+ int httpclient_parser_init(httpclient_parser *parser) {
97
+ int cs = 0;
98
+ %% write init;
99
+ parser->cs = cs;
100
+ parser->body_start = 0;
101
+ parser->content_len = 0;
102
+ parser->mark = 0;
103
+ parser->nread = 0;
104
+ parser->field_len = 0;
105
+ parser->field_start = 0;
106
+
107
+ return(1);
108
+ }
109
+
110
+
111
+ /** exec **/
112
+ size_t httpclient_parser_execute(httpclient_parser *parser, const char *buffer, size_t len, size_t off) {
113
+ const char *p, *pe;
114
+ int cs = parser->cs;
115
+
116
+ assert(off <= len && "offset past end of buffer");
117
+
118
+ p = buffer+off;
119
+ pe = buffer+len;
120
+
121
+ assert(*pe == '\0' && "pointer does not end on NUL");
122
+ assert(pe - p == len - off && "pointers aren't same distance");
123
+
124
+
125
+ %% write exec;
126
+
127
+ parser->cs = cs;
128
+ parser->nread += p - (buffer + off);
129
+
130
+ assert(p <= pe && "buffer overflow after parsing execute");
131
+ assert(parser->nread <= len && "nread longer than length");
132
+ assert(parser->body_start <= len && "body starts after buffer end");
133
+ assert(parser->mark < len && "mark is after buffer end");
134
+ assert(parser->field_len <= len && "field has length longer than whole buffer");
135
+ assert(parser->field_start < len && "field starts after buffer end");
136
+
137
+ if(parser->body_start) {
138
+ /* final \r\n combo encountered so stop right here */
139
+ %%write eof;
140
+ parser->nread++;
141
+ }
142
+
143
+ return(parser->nread);
144
+ }
145
+
146
+ int httpclient_parser_finish(httpclient_parser *parser)
147
+ {
148
+ int cs = parser->cs;
149
+
150
+ %%write eof;
151
+
152
+ parser->cs = cs;
153
+
154
+ if (httpclient_parser_has_error(parser) ) {
155
+ return -1;
156
+ } else if (httpclient_parser_is_finished(parser) ) {
157
+ return 1;
158
+ } else {
159
+ return 0;
160
+ }
161
+ }
162
+
163
+ int httpclient_parser_has_error(httpclient_parser *parser) {
164
+ return parser->cs == httpclient_parser_error;
165
+ }
166
+
167
+ int httpclient_parser_is_finished(httpclient_parser *parser) {
168
+ return parser->cs == httpclient_parser_first_final;
169
+ }
@@ -0,0 +1,498 @@
1
+ require 'http11_client'
2
+ require 'socket'
3
+ require 'stringio'
4
+ require 'rfuzz/stats'
5
+
6
+ module RFuzz
7
+
8
+
9
+ # A simple hash is returned for each request made by HttpClient with
10
+ # the headers that were given by the server for that request. Attached
11
+ # to this are four attributes you can play with:
12
+ #
13
+ # * http_reason
14
+ # * http_version
15
+ # * http_status
16
+ # * http_body
17
+ #
18
+ # These are set internally by the Ragel/C parser so they're very fast
19
+ # and pretty much C voodoo. You can modify them without fear once you get
20
+ # the response.
21
+ class HttpResponse < Hash
22
+ # The reason returned in the http response ("OK","File not found",etc.)
23
+ attr_accessor :http_reason
24
+
25
+ # The HTTP version returned.
26
+ attr_accessor :http_version
27
+
28
+ # The status code (as a string!)
29
+ attr_accessor :http_status
30
+
31
+ # The http body of the response, in the raw
32
+ attr_accessor :http_body
33
+
34
+ # When parsing chunked encodings this is set
35
+ attr_accessor :http_chunk_size
36
+ end
37
+
38
+ # A mixin that has most of the HTTP encoding methods you need to work
39
+ # with the protocol. It's used by HttpClient, but you can use it
40
+ # as well.
41
+ module HttpEncoding
42
+
43
+ # Converts a Hash of cookies to the appropriate simple cookie
44
+ # headers.
45
+ def encode_cookies(cookies)
46
+ result = ""
47
+ cookies.each do |k,v|
48
+ if v.kind_of? Array
49
+ v.each {|x| result += encode_field("Cookie", encode_param(k,x)) }
50
+ else
51
+ result += encode_field("Cookie", encode_param(k,v))
52
+ end
53
+ end
54
+ return result
55
+ end
56
+
57
+ # Encode HTTP header fields of "k: v\r\n"
58
+ def encode_field(k,v)
59
+ "#{k}: #{v}\r\n"
60
+ end
61
+
62
+ # Encodes the headers given in the hash returning a string
63
+ # you can use.
64
+ def encode_headers(head)
65
+ result = ""
66
+ head.each do |k,v|
67
+ if v.kind_of? Array
68
+ v.each {|x| result += encode_field(k,x) }
69
+ else
70
+ result += encode_field(k,v)
71
+ end
72
+ end
73
+ return result
74
+ end
75
+
76
+ # URL encodes a single k=v parameter.
77
+ def encode_param(k,v)
78
+ escape(k) + "=" + escape(v)
79
+ end
80
+
81
+ # Takes a query string and encodes it as a URL encoded
82
+ # set of key=value pairs with & separating them.
83
+ def encode_query(uri, query)
84
+ params = []
85
+
86
+ if query
87
+ query.each do |k,v|
88
+ if v.kind_of? Array
89
+ v.each {|x| params << encode_param(k,x) }
90
+ else
91
+ params << encode_param(k,v)
92
+ end
93
+ end
94
+
95
+ uri += "?" + params.join('&')
96
+ end
97
+
98
+ return uri
99
+ end
100
+
101
+ # HTTP is kind of retarded that you have to specify
102
+ # a Host header, but if you include port 80 then further
103
+ # redirects will tack on the :80 which is annoying.
104
+ def encode_host(host, port)
105
+ "#{host}" + (port.to_i != 80 ? ":#{port}" : "")
106
+ end
107
+
108
+ # Performs URI escaping so that you can construct proper
109
+ # query strings faster. Use this rather than the cgi.rb
110
+ # version since it's faster. (Stolen from Camping).
111
+ def escape(s)
112
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
113
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
114
+ }.tr(' ', '+')
115
+ end
116
+
117
+
118
+ # Unescapes a URI escaped string. (Stolen from Camping).
119
+ def unescape(s)
120
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
121
+ [$1.delete('%')].pack('H*')
122
+ }
123
+ end
124
+
125
+ # Parses a query string by breaking it up at the '&'
126
+ # and ';' characters. You can also use this to parse
127
+ # cookies by changing the characters used in the second
128
+ # parameter (which defaults to '&;'.
129
+ def query_parse(qs, d = '&;')
130
+ params = {}
131
+ (qs||'').split(/[#{d}] */n).inject(params) { |h,p|
132
+ k, v=unescape(p).split('=',2)
133
+ if cur = params[k]
134
+ if cur.class == Array
135
+ params[k] << v
136
+ else
137
+ params[k] = [cur, v]
138
+ end
139
+ else
140
+ params[k] = v
141
+ end
142
+ }
143
+
144
+ return params
145
+ end
146
+ end
147
+
148
+
149
+ # The actual HttpClient that does the work with the thinnest
150
+ # layer between you and the protocol. All exceptions and leaks
151
+ # are allowed to pass through since those are important when
152
+ # testing. It doesn't pretend to be a full client, but instead
153
+ # is just enough client to track cookies, form proper HTTP requests,
154
+ # and return HttpResponse hashes with the results.
155
+ #
156
+ # It's designed so that you create one client, and then you work it
157
+ # with a minimum of parameters as you need. The initialize method
158
+ # lets you pass in defaults for most of the parameters you'll need,
159
+ # and you can simple call the method you want and it'll be translated
160
+ # to an HTTP method (client.get => GET, client.foobar = FOOBAR).
161
+ #
162
+ # Here's a few examples:
163
+ #
164
+ # client = HttpClient.new(:head => {"X-DefaultHeader" => "ONE"})
165
+ # resp = client.post("/test")
166
+ # resp = client.post("/test", :head => {"X-TestSend" => "Status"}, :body => "TEST BODY")
167
+ # resp = client.put("/testput", :query => {"q" => "test"}, :body => "SOME JUNK")
168
+ # client.reset
169
+ #
170
+ # The HttpClient.reset call clears cookies that are maintained.
171
+ #
172
+ # It uses method_missing to do the translation of .put to "PUT /testput HTTP/1.1"
173
+ # so you can get into trouble if you're calling unknown methods on it. By
174
+ # default the methods are PUT, GET, POST, DELETE, HEAD. You can change
175
+ # the allowed methods by passing :allowed_methods => [:put, :get, ..] to
176
+ # the initialize for the object.
177
+ #
178
+ # == Notifications
179
+ #
180
+ # You can register a "notifier" with the client that will get called when
181
+ # different events happen. Right now the Notifier class just has a few
182
+ # functions for the common parts of an HTTP request that each take a
183
+ # symbol and some extra parameters. See RFuzz::Notifier for more
184
+ # information.
185
+ #
186
+ # == Parameters
187
+ #
188
+ # :head => {K => V} or {K => [V1,V2]}
189
+ # :query => {K => V} or {K => [V1,V2]}
190
+ # :body => "some body" (you must encode for now)
191
+ # :cookies => {K => V} or {K => [V1, V2]}
192
+ # :allowed_methods => [:put, :get, :post, :delete, :head]
193
+ # :notifier => Notifier.new
194
+ # :redirect => false (give it a number and it'll follow redirects for that count)
195
+ #
196
+ class HttpClient
197
+ include HttpEncoding
198
+
199
+ TRANSFER_ENCODING="TRANSFER_ENCODING"
200
+ CONTENT_LENGTH="CONTENT_LENGTH"
201
+ SET_COOKIE="SET_COOKIE"
202
+ LOCATION="LOCATION"
203
+ HOST="HOST"
204
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
205
+
206
+ # Access to the host, port, default options, and cookies currently in play
207
+ attr_accessor :host, :port, :options, :cookies, :allowed_methods, :notifier
208
+
209
+ # Doesn't make the connect until you actually call a .put,.get, etc.
210
+ def initialize(host, port, options = {})
211
+ @options = options
212
+ @host = host
213
+ @port = port
214
+ @cookies = options[:cookies] || {}
215
+ @allowed_methods = options[:allowed_methods] || [:put, :get, :post, :delete, :head]
216
+ @notifier = options[:notifier]
217
+ @redirect = options[:redirect] || false
218
+ end
219
+
220
+
221
+ # Builds a full request from the method, uri, req, and @cookies
222
+ # using the default @options and writes it to out (should be an IO).
223
+ #
224
+ # It returns the body that the caller should use (based on defaults
225
+ # resolution).
226
+ def build_request(out, method, uri, req)
227
+ ops = @options.merge(req)
228
+ query = ops[:query]
229
+
230
+ # merge head differently since that's typically what they mean
231
+ head = req[:head] || {}
232
+ head = ops[:head].merge(head) if ops[:head]
233
+
234
+ # setup basic headers we always need
235
+ head[HOST] = encode_host(@host,@port)
236
+ head[CONTENT_LENGTH] = ops[:body] ? ops[:body].length : 0
237
+
238
+ # blast it out
239
+ out.write(HTTP_REQUEST_HEADER % [method, encode_query(uri,query)])
240
+ out.write(encode_headers(head))
241
+ out.write(encode_cookies(@cookies.merge(req[:cookies] || {})))
242
+ out.write("\r\n")
243
+ ops[:body] || ""
244
+ end
245
+
246
+ def read_chunks(input, out, parser)
247
+ begin
248
+ until input.closed?
249
+ parser.reset
250
+ chunk = HttpResponse.new
251
+ line = input.readline("\r\n")
252
+ nread = parser.execute(chunk, line, 0)
253
+
254
+ if !parser.finished?
255
+ # tried to read this header but couldn't
256
+ return :incomplete_header, line
257
+ end
258
+
259
+ size = chunk.http_chunk_size ? chunk.http_chunk_size.to_i(base=16) : 0
260
+
261
+ if size == 0
262
+ return :finished, nil
263
+ end
264
+ remain = size -out.write(input.read(size))
265
+ return :incomplete_body, remain if remain > 0
266
+
267
+ line = input.read(2)
268
+ if line.nil? or line.length < 2
269
+ return :incomplete_trailer, line
270
+ elsif line != "\r\n"
271
+ raise HttpClientParserError.new("invalid chunked encoding trailer")
272
+ end
273
+ end
274
+ rescue EOFError
275
+ # this is thrown when the header read is attempted and
276
+ # there's nothing in the buffer
277
+ return :eof_error, nil
278
+ end
279
+ end
280
+
281
+ def read_chunked_encoding(resp, sock, parser)
282
+ out = StringIO.new
283
+ input = StringIO.new(resp.http_body)
284
+
285
+ # read from the http body first, then continue at the socket
286
+ status, result = read_chunks(input, out, parser)
287
+
288
+ case status
289
+ when :incomplete_trailer
290
+ if result.nil?
291
+ sock.read(2)
292
+ else
293
+ sock.read(result.length - 2)
294
+ end
295
+ when :incomplete_body
296
+ out.write(sock.read(result)) # read the remaining
297
+ sock.read(2)
298
+ when :incomplete_header
299
+ # push what we read back onto the socket, but backwards
300
+ result.reverse!
301
+ result.each_byte {|b| sock.ungetc(b) }
302
+ when :finished
303
+ # all done, get out
304
+ out.rewind; return out.read
305
+ when :eof_error
306
+ # read everything we could, ignore
307
+ end
308
+
309
+ # then continue reading them from the socket
310
+ status, result = read_chunks(sock, out, parser)
311
+
312
+ # and now the http_body is the chunk
313
+ out.rewind; return out.read
314
+ end
315
+
316
+ # Reads an HTTP response from the given socket. It uses
317
+ # readpartial which only appeared in Ruby 1.8.4. The result
318
+ # is a fully formed HttpResponse object for you to play with.
319
+ #
320
+ # As with other methods in this class it doesn't stop any exceptions
321
+ # from reaching your code. It's for experts who want these exceptions
322
+ # so either write a wrapper, use net/http, or deal with it on your end.
323
+ def read_response(sock)
324
+ data, resp = nil, nil
325
+ parser = HttpClientParser.new
326
+ resp = HttpResponse.new
327
+
328
+ notify :read_header do
329
+ data = sock.readpartial(1024)
330
+ nread = parser.execute(resp, data, 0)
331
+
332
+ while not parser.finished?
333
+ data += sock.readpartial(1024)
334
+ nread += parser.execute(resp, data, nread)
335
+ end
336
+ end
337
+
338
+ notify :read_body do
339
+ if resp[TRANSFER_ENCODING] and resp[TRANSFER_ENCODING].index("chunked")
340
+ resp.http_body = read_chunked_encoding(resp, sock, parser)
341
+ elsif resp[CONTENT_LENGTH]
342
+ cl = resp[CONTENT_LENGTH].to_i
343
+ if cl - resp.http_body.length > 0
344
+ resp.http_body += sock.read(cl - resp.http_body.length)
345
+ elsif cl < resp.http_body.length
346
+ STDERR.puts "Web site sucks, they said Content-Length: #{cl}, but sent a longer body length: #{resp.http_body.length}"
347
+ end
348
+ else
349
+ resp.http_body += sock.read
350
+ end
351
+ end
352
+
353
+ if resp[SET_COOKIE]
354
+ cookies = query_parse(resp[SET_COOKIE], ';,')
355
+ @cookies.merge! cookies
356
+ @cookies.delete "path"
357
+ end
358
+
359
+ notify :close do
360
+ sock.close
361
+ end
362
+
363
+ resp
364
+ end
365
+
366
+ # Does the socket connect and then build_request, read_response
367
+ # calls finally returning the result.
368
+ def send_request(method, uri, req)
369
+ begin
370
+ sock = nil
371
+ notify :connect do
372
+ sock = TCPSocket.new(@host, @port)
373
+ end
374
+
375
+ out = StringIO.new
376
+ body = build_request(out, method, uri, req)
377
+
378
+ notify :send_request do
379
+ sock.write(out.string + body)
380
+ sock.flush
381
+ end
382
+
383
+ return read_response(sock)
384
+ rescue Object
385
+ raise $!
386
+ ensure
387
+ sock.close unless (!sock or sock.closed?)
388
+ end
389
+ end
390
+
391
+
392
+ # Translates unknown function calls into PUT, GET, POST, DELETE, HEAD
393
+ # methods. The allowed HTTP methods allowed are restricted by the
394
+ # @allowed_methods attribute which you can set after construction or
395
+ # during construction with :allowed_methods => [:put, :get, ...]
396
+ def method_missing(symbol, *args)
397
+ if @allowed_methods.include? symbol
398
+ method = symbol.to_s.upcase
399
+ resp = send_request(method, args[0], args[1] || {})
400
+ resp = redirect(symbol, resp) if @redirect
401
+
402
+ return resp
403
+ else
404
+ raise "Invalid method: #{symbol}"
405
+ end
406
+ end
407
+
408
+ # Keeps doing requests until it doesn't receive a 3XX request.
409
+ def redirect(method, resp, *args)
410
+ @redirect.times do
411
+ break if resp.http_status.index("3") != 0
412
+
413
+ host = encode_host(@host,@port)
414
+ location = resp[LOCATION]
415
+
416
+ if location.index(host) == 0
417
+ # begins with the host so strip that off
418
+ location = location[host.length .. -1]
419
+ end
420
+
421
+ @notifier.redirect(:begins) if @notifier
422
+ resp = self.send(method, location, *args)
423
+ @notifier.redirect(:ends) if @notifier
424
+ end
425
+
426
+ return resp
427
+ end
428
+
429
+ # Clears out the cookies in use so far in order to get
430
+ # a clean slate.
431
+ def reset
432
+ @cookies.clear
433
+ end
434
+
435
+
436
+ # Sends the notifications to the registered notifier, taking
437
+ # a block that it runs doing the :begins, :ends states
438
+ # around it.
439
+ #
440
+ # It also catches errors transparently in order to call
441
+ # the notifier when an attempt fails.
442
+ def notify(event)
443
+ @notifier.send(event, :begins) if @notifier
444
+
445
+ begin
446
+ yield
447
+ @notifier.send(event, :ends) if @notifier
448
+ rescue Object
449
+ @notifier.send(event, :error) if @notifier
450
+ raise $!
451
+ end
452
+ end
453
+ end
454
+
455
+
456
+
457
+ # This simple class can be registered with an HttpClient and it'll
458
+ # get called when different parts of the HTTP request happen.
459
+ # Each function represents a different event, and the state parameter
460
+ # is a symbol of consisting of:
461
+ #
462
+ # :begins -- event begins.
463
+ # :error -- event caused exception.
464
+ # :ends -- event finished (not called if error).
465
+ #
466
+ # These calls are made synchronously so you can throttle
467
+ # the client by sleeping inside them and can track timing
468
+ # data.
469
+ class Notifier
470
+ # Fired right before connecting and right after the connection.
471
+ def connect(state)
472
+ end
473
+
474
+ # Before and after the full request is actually sent. This may
475
+ # become "send_header" and "send_body", but right now the whole
476
+ # blob is shot out in one chunk for efficiency.
477
+ def send_request(state)
478
+ end
479
+
480
+ # Called whenever a HttpClient.redirect is done and there
481
+ # are redirects to follow. You can use a notifier to detect
482
+ # that you're doing to many and throw an abort.
483
+ def redirect(state)
484
+ end
485
+
486
+ # Before and after the header is finally read.
487
+ def read_header(state)
488
+ end
489
+
490
+ # Before and after the body is ready.
491
+ def read_body(state)
492
+ end
493
+
494
+ # Before and after the client closes with the server.
495
+ def close(state)
496
+ end
497
+ end
498
+ end