rfuzz 0.6

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