rfuzz 0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +55 -0
- data/LICENSE +55 -0
- data/README +252 -0
- data/Rakefile +48 -0
- data/doc/rdoc/classes/RFuzz.html +146 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.html +481 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000010.html +24 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000011.html +34 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000012.html +49 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000013.html +49 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000014.html +57 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000015.html +37 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000016.html +26 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000017.html +34 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000018.html +18 -0
- data/doc/rdoc/classes/RFuzz/HttpClient.src/M000019.html +26 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.html +294 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000001.html +26 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000002.html +18 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000003.html +26 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000004.html +18 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000005.html +32 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000006.html +18 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000007.html +20 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000008.html +20 -0
- data/doc/rdoc/classes/RFuzz/HttpEncoding.src/M000009.html +32 -0
- data/doc/rdoc/classes/RFuzz/HttpResponse.html +180 -0
- data/doc/rdoc/classes/RFuzz/Notifier.html +252 -0
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000044.html +17 -0
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000045.html +17 -0
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000046.html +17 -0
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000047.html +17 -0
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000048.html +17 -0
- data/doc/rdoc/classes/RFuzz/Notifier.src/M000049.html +17 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.html +362 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000032.html +21 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000033.html +23 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000036.html +22 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000037.html +20 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000038.html +22 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000039.html +20 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000040.html +18 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000041.html +18 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000042.html +22 -0
- data/doc/rdoc/classes/RFuzz/RandomGenerator.src/M000043.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.html +383 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000056.html +19 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000057.html +23 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000058.html +26 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000059.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000060.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000061.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000062.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000063.html +19 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000064.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000065.html +23 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000066.html +18 -0
- data/doc/rdoc/classes/RFuzz/Sampler.src/M000067.html +20 -0
- data/doc/rdoc/classes/RFuzz/Session.html +415 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000020.html +31 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000021.html +18 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000022.html +18 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000023.html +34 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000024.html +19 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000025.html +19 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000026.html +26 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000027.html +29 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000028.html +19 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000029.html +18 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000030.html +18 -0
- data/doc/rdoc/classes/RFuzz/Session.src/M000031.html +23 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.html +242 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000050.html +19 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000051.html +19 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000052.html +18 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000053.html +18 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000054.html +28 -0
- data/doc/rdoc/classes/RFuzz/StatsTracker.src/M000055.html +18 -0
- data/doc/rdoc/created.rid +1 -0
- data/doc/rdoc/files/COPYING.html +168 -0
- data/doc/rdoc/files/LICENSE.html +168 -0
- data/doc/rdoc/files/README.html +473 -0
- data/doc/rdoc/files/lib/rfuzz/client_rb.html +111 -0
- data/doc/rdoc/files/lib/rfuzz/random_rb.html +116 -0
- data/doc/rdoc/files/lib/rfuzz/rfuzz_rb.html +109 -0
- data/doc/rdoc/files/lib/rfuzz/session_rb.html +111 -0
- data/doc/rdoc/files/lib/rfuzz/stats_rb.html +113 -0
- data/doc/rdoc/fr_class_index.html +35 -0
- data/doc/rdoc/fr_file_index.html +34 -0
- data/doc/rdoc/fr_method_index.html +93 -0
- data/doc/rdoc/index.html +24 -0
- data/doc/rdoc/rdoc-style.css +208 -0
- data/examples/amazon_headers.rb +38 -0
- data/examples/hpricot_pudding.rb +22 -0
- data/examples/kill_routes.rb +26 -0
- data/examples/mongrel_test_suite/lib/gen.rb +24 -0
- data/examples/mongrel_test_suite/test/camping/static_files.rb +9 -0
- data/examples/mongrel_test_suite/test/camping/upload_file.rb +9 -0
- data/examples/mongrel_test_suite/test/camping/upload_progress.rb +9 -0
- data/examples/mongrel_test_suite/test/http/base_protocol.rb +23 -0
- data/examples/mongrel_test_suite/test/nitro/upload_file.rb +9 -0
- data/examples/mongrel_test_suite/test/nitro/upload_progress.rb +9 -0
- data/examples/mongrel_test_suite/test/rails/static_files.rb +9 -0
- data/examples/mongrel_test_suite/test/rails/upload_file.rb +9 -0
- data/examples/mongrel_test_suite/test/rails/upload_progress.rb +9 -0
- data/examples/perftest.rb +30 -0
- data/ext/fuzzrnd/ext_help.h +14 -0
- data/ext/fuzzrnd/extconf.rb +6 -0
- data/ext/fuzzrnd/fuzzrnd.c +149 -0
- data/ext/http11_client/ext_help.h +14 -0
- data/ext/http11_client/extconf.rb +6 -0
- data/ext/http11_client/http11_client.c +288 -0
- data/ext/http11_client/http11_parser.c +629 -0
- data/ext/http11_client/http11_parser.h +46 -0
- data/ext/http11_client/http11_parser.rl +169 -0
- data/lib/rfuzz/client.rb +498 -0
- data/lib/rfuzz/random.rb +110 -0
- data/lib/rfuzz/rfuzz.rb +12 -0
- data/lib/rfuzz/session.rb +154 -0
- data/lib/rfuzz/stats.rb +159 -0
- data/resources/defaults.yaml +2 -0
- data/resources/words.txt +3310 -0
- data/test/coverage/index.html +388 -0
- data/test/coverage/lib-rfuzz-client_rb.html +1127 -0
- data/test/coverage/lib-rfuzz-random_rb.html +739 -0
- data/test/coverage/lib-rfuzz-session_rb.html +783 -0
- data/test/coverage/lib-rfuzz-stats_rb.html +788 -0
- data/test/server.rb +101 -0
- data/test/test_client.rb +164 -0
- data/test/test_fuzzrnd.rb +31 -0
- data/test/test_httpparser.rb +48 -0
- data/test/test_random.rb +75 -0
- data/test/test_session.rb +33 -0
- data/test/test_stats.rb +45 -0
- data/tools/rakehelp.rb +119 -0
- 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
|
+
}
|
data/lib/rfuzz/client.rb
ADDED
@@ -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
|