fast_http 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
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
+ element_cb last_chunk;
36
+
37
+
38
+ } httpclient_parser;
39
+
40
+ int httpclient_parser_init(httpclient_parser *parser);
41
+ int httpclient_parser_finish(httpclient_parser *parser);
42
+ size_t httpclient_parser_execute(httpclient_parser *parser, const char *data, size_t len, size_t off);
43
+ int httpclient_parser_has_error(httpclient_parser *parser);
44
+ int httpclient_parser_is_finished(httpclient_parser *parser);
45
+
46
+ #define httpclient_parser_nread(parser) (parser)->nread
47
+
48
+ #endif
@@ -0,0 +1,173 @@
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 last_chunk {
54
+ parser->last_chunk(parser->data, NULL, 0);
55
+ }
56
+
57
+ action done {
58
+ parser->body_start = fpc - buffer + 1;
59
+ if(parser->header_done != NULL)
60
+ parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
61
+ fbreak;
62
+ }
63
+
64
+ # line endings
65
+ CRLF = "\r\n";
66
+
67
+ # character types
68
+ CTL = (cntrl | 127);
69
+ tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
70
+
71
+ # elements
72
+ token = (ascii -- (CTL | tspecials));
73
+
74
+ Reason_Phrase = (any -- CRLF)+ >mark %reason_phrase;
75
+ Status_Code = digit+ >mark %status_code;
76
+ http_number = (digit+ "." digit+) ;
77
+ HTTP_Version = ("HTTP/" http_number) >mark %http_version ;
78
+ Status_Line = HTTP_Version " " Status_Code " " Reason_Phrase :> CRLF;
79
+
80
+ field_name = token+ >start_field %write_field;
81
+ field_value = any* >start_value %write_value;
82
+ message_header = field_name ":" " "* field_value :> CRLF;
83
+
84
+ Response = Status_Line (message_header)* (CRLF @done);
85
+
86
+ chunk_ext_val = token+;
87
+ chunk_ext_name = token+;
88
+ chunk_extension = (";" chunk_ext_name >start_field %write_field %start_value ("=" chunk_ext_val >start_value)? %write_value )*;
89
+ last_chunk = "0"? chunk_extension :> (CRLF @last_chunk @done);
90
+ chunk_size = xdigit+;
91
+ chunk = chunk_size >mark %chunk_size chunk_extension :> (CRLF @done);
92
+ Chunked_Header = (chunk | last_chunk);
93
+
94
+ main := Response | Chunked_Header;
95
+ }%%
96
+
97
+ /** Data **/
98
+ %% write data;
99
+
100
+ int httpclient_parser_init(httpclient_parser *parser) {
101
+ int cs = 0;
102
+ %% write init;
103
+ parser->cs = cs;
104
+ parser->body_start = 0;
105
+ parser->content_len = 0;
106
+ parser->mark = 0;
107
+ parser->nread = 0;
108
+ parser->field_len = 0;
109
+ parser->field_start = 0;
110
+
111
+ return(1);
112
+ }
113
+
114
+
115
+ /** exec **/
116
+ size_t httpclient_parser_execute(httpclient_parser *parser, const char *buffer, size_t len, size_t off) {
117
+ const char *p, *pe;
118
+ int cs = parser->cs;
119
+
120
+ assert(off <= len && "offset past end of buffer");
121
+
122
+ p = buffer+off;
123
+ pe = buffer+len;
124
+
125
+ assert(*pe == '\0' && "pointer does not end on NUL");
126
+ assert(pe - p == len - off && "pointers aren't same distance");
127
+
128
+
129
+ %% write exec;
130
+
131
+ parser->cs = cs;
132
+ parser->nread += p - (buffer + off);
133
+
134
+ assert(p <= pe && "buffer overflow after parsing execute");
135
+ assert(parser->nread <= len && "nread longer than length");
136
+ assert(parser->body_start <= len && "body starts after buffer end");
137
+ assert(parser->mark < len && "mark is after buffer end");
138
+ assert(parser->field_len <= len && "field has length longer than whole buffer");
139
+ assert(parser->field_start < len && "field starts after buffer end");
140
+
141
+ if(parser->body_start) {
142
+ /* final \r\n combo encountered so stop right here */
143
+ %%write eof;
144
+ parser->nread++;
145
+ }
146
+
147
+ return(parser->nread);
148
+ }
149
+
150
+ int httpclient_parser_finish(httpclient_parser *parser)
151
+ {
152
+ int cs = parser->cs;
153
+
154
+ %%write eof;
155
+
156
+ parser->cs = cs;
157
+
158
+ if (httpclient_parser_has_error(parser) ) {
159
+ return -1;
160
+ } else if (httpclient_parser_is_finished(parser) ) {
161
+ return 1;
162
+ } else {
163
+ return 0;
164
+ }
165
+ }
166
+
167
+ int httpclient_parser_has_error(httpclient_parser *parser) {
168
+ return parser->cs == httpclient_parser_error;
169
+ }
170
+
171
+ int httpclient_parser_is_finished(httpclient_parser *parser) {
172
+ return parser->cs == httpclient_parser_first_final;
173
+ }
@@ -0,0 +1,12 @@
1
+ have_library: checking for main() in -lc... -------------------- yes
2
+
3
+ "gcc -o conftest -I. -I/usr/local/lib/ruby/1.8/i686-darwin9.8.0 -I. -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -g -O2 -pipe -fno-common conftest.c -L. -L/usr/local/lib -L. -lruby-static -lc -ldl -lobjc "
4
+ checked program was:
5
+ /* begin */
6
+ 1: /*top*/
7
+ 2: int main() { return 0; }
8
+ 3: int t() { void ((*volatile p)()); p = (void ((*)()))main; return 0; }
9
+ /* end */
10
+
11
+ --------------------
12
+
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), *%w[.. ext http11_client http11_client])
2
+ require File.join(File.dirname(__FILE__), *%w[fast_http client])
3
+ require File.join(File.dirname(__FILE__), *%w[fast_http pushbackio])
@@ -0,0 +1,441 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+
4
+ module FastHttp
5
+
6
+ # Thrown for errors not related to the protocol format (HttpClientParserError are
7
+ # thrown for that).
8
+ class HttpClientError < StandardError; end
9
+
10
+ # A simple hash is returned for each request made by HttpClient with
11
+ # the headers that were given by the server for that request.
12
+ class HttpResponse < Hash
13
+ # The reason returned in the http response ("OK","File not found",etc.)
14
+ attr_accessor :http_reason
15
+
16
+ # The HTTP version returned.
17
+ attr_accessor :http_version
18
+
19
+ # The status code (as a string!)
20
+ attr_accessor :http_status
21
+
22
+ # The http body of the response, in the raw
23
+ attr_accessor :http_body
24
+
25
+ # When parsing chunked encodings this is set
26
+ attr_accessor :http_chunk_size
27
+
28
+ # The actual chunks taken from the chunked encoding
29
+ attr_accessor :raw_chunks
30
+
31
+ # Converts the http_chunk_size string properly
32
+ def chunk_size
33
+ if @chunk_size == nil
34
+ @chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
35
+ end
36
+
37
+ @chunk_size
38
+ end
39
+
40
+ # true if this is the last chunk, nil otherwise (false)
41
+ def last_chunk?
42
+ @last_chunk || chunk_size == 0
43
+ end
44
+
45
+ # Easier way to find out if this is a chunked encoding
46
+ def chunked_encoding?
47
+ /chunked/i === self[HttpClient::TRANSFER_ENCODING]
48
+ end
49
+ end
50
+
51
+ # A mixin that has most of the HTTP encoding methods you need to work
52
+ # with the protocol. It's used by HttpClient, but you can use it
53
+ # as well.
54
+ module HttpEncoding
55
+ COOKIE="Cookie"
56
+ FIELD_ENCODING="%s: %s\r\n"
57
+
58
+ # Converts a Hash of cookies to the appropriate simple cookie
59
+ # headers.
60
+ def encode_cookies(cookies)
61
+ result = ""
62
+ cookies.each do |k,v|
63
+ if v.kind_of? Array
64
+ v.each {|x| result << encode_field(COOKIE, encode_param(k,x)) }
65
+ else
66
+ result << encode_field(COOKIE, encode_param(k,v))
67
+ end
68
+ end
69
+ return result
70
+ end
71
+
72
+ # Encode HTTP header fields of "k: v\r\n"
73
+ def encode_field(k,v)
74
+ FIELD_ENCODING % [k,v]
75
+ end
76
+
77
+ # Encodes the headers given in the hash returning a string
78
+ # you can use.
79
+ def encode_headers(head)
80
+ @headers_cache = {}
81
+ @headers_cache[head] ||
82
+ result = ""
83
+ head.each do |k,v|
84
+ if v.kind_of? Array
85
+ v.each {|x| result << encode_field(k,x) }
86
+ else
87
+ result << encode_field(k,v)
88
+ end
89
+ end
90
+ return result
91
+ end
92
+
93
+ # URL encodes a single k=v parameter.
94
+ def encode_param(k,v)
95
+ escape(k) + "=" + escape(v)
96
+ end
97
+
98
+ # Takes a query string and encodes it as a URL encoded
99
+ # set of key=value pairs with & separating them.
100
+ def encode_query(uri, query)
101
+ params = []
102
+
103
+ if query
104
+ query.each do |k,v|
105
+ if v.kind_of? Array
106
+ v.each {|x| params << encode_param(k,x) }
107
+ else
108
+ params << encode_param(k,v)
109
+ end
110
+ end
111
+
112
+ uri += "?" + params.join('&')
113
+ end
114
+
115
+ return uri
116
+ end
117
+
118
+ # HTTP is kind of retarded that you have to specify
119
+ # a Host header, but if you include port 80 then further
120
+ # redirects will tack on the :80 which is annoying.
121
+ def encode_host(host, port)
122
+ host + (port.to_i != 80 ? ":#{port}" : "")
123
+ end
124
+
125
+ # Escapes a URI.
126
+ def escape(s)
127
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
128
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
129
+ }.tr(' ', '+')
130
+ end
131
+
132
+
133
+ # Unescapes a URI escaped string.
134
+ def unescape(s)
135
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
136
+ [$1.delete('%')].pack('H*')
137
+ }
138
+ end
139
+
140
+ # Parses a query string by breaking it up at the '&'
141
+ # and ';' characters. You can also use this to parse
142
+ # cookies by changing the characters used in the second
143
+ # parameter (which defaults to '&;'.
144
+ def query_parse(qs, d = '&;')
145
+ params = {}
146
+ (qs||'').split(/[#{d}] */n).inject(params) { |h,p|
147
+ k, v=unescape(p).split('=',2)
148
+ if cur = params[k]
149
+ if cur.class == Array
150
+ params[k] << v
151
+ else
152
+ params[k] = [cur, v]
153
+ end
154
+ else
155
+ params[k] = v
156
+ end
157
+ }
158
+
159
+ return params
160
+ end
161
+ end
162
+
163
+
164
+ # The actual HttpClient that does the work with the thinnest
165
+ # layer between you and the protocol. All exceptions and leaks
166
+ # are allowed to pass through since those are important when
167
+ # testing. It doesn't pretend to be a full client, but instead
168
+ # is just enough client to track cookies, form proper HTTP requests,
169
+ # and return HttpResponse hashes with the results.
170
+ #
171
+ # It's designed so that you create one client, and then you work it
172
+ # with a minimum of parameters as you need. The initialize method
173
+ # lets you pass in defaults for most of the parameters you'll need,
174
+ # and you can simple call the method you want and it'll be translated
175
+ # to an HTTP method (client.get => GET, client.foobar = FOOBAR).
176
+ #
177
+ # Here's a few examples:
178
+ #
179
+ # client = HttpClient.new(:head => {"X-DefaultHeader" => "ONE"})
180
+ # resp = client.post("/test")
181
+ # resp = client.post("/test", :head => {"X-TestSend" => "Status"}, :body => "TEST BODY")
182
+ # resp = client.put("/testput", :query => {"q" => "test"}, :body => "SOME JUNK")
183
+ # client.reset
184
+ #
185
+ # The HttpClient.reset call clears cookies that are maintained.
186
+ #
187
+ # It uses method_missing to do the translation of .put to "PUT /testput HTTP/1.1"
188
+ # so you can get into trouble if you're calling unknown methods on it. By
189
+ # default the methods are PUT, GET, POST, DELETE, HEAD. You can change
190
+ # the allowed methods by passing :allowed_methods => [:put, :get, ..] to
191
+ # the initialize for the object.
192
+ #
193
+ # == Notifications
194
+ #
195
+ # You can register a "notifier" with the client that will get called when
196
+ # different events happen. Right now the Notifier class just has a few
197
+ # functions for the common parts of an HTTP request that each take a
198
+ # symbol and some extra parameters. See FastHttp::Notifier for more
199
+ # information.
200
+ #
201
+ # == Parameters
202
+ #
203
+ # :head => {K => V} or {K => [V1,V2]}
204
+ # :query => {K => V} or {K => [V1,V2]}
205
+ # :body => "some body" (you must encode for now)
206
+ # :cookies => {K => V} or {K => [V1, V2]}
207
+ # :allowed_methods => [:put, :get, :post, :delete, :head]
208
+ # :notifier => Notifier.new
209
+ # :redirect => false (give it a number and it'll follow redirects for that count)
210
+ #
211
+ class HttpClient
212
+ include HttpEncoding
213
+
214
+ TRANSFER_ENCODING="TRANSFER_ENCODING"
215
+ CONTENT_LENGTH="CONTENT_LENGTH"
216
+ SET_COOKIE="SET_COOKIE"
217
+ LOCATION="LOCATION"
218
+ HOST="HOST"
219
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
220
+ REQ_CONTENT_LENGTH="Content-Length"
221
+ REQ_HOST="Host"
222
+ CHUNK_SIZE=1024 * 16
223
+ CRLF="\r\n"
224
+
225
+ # Access to the host, port, default options, and cookies currently in play
226
+ attr_accessor :host, :port, :options, :cookies, :allowed_methods, :sock
227
+
228
+ # Doesn't make the connect until you actually call a .put,.get, etc.
229
+ def initialize(host, port, options = {})
230
+ @options = options
231
+ @host = host
232
+ @port = port
233
+ @cookies = options[:cookies]
234
+ @allowed_methods = options[:allowed_methods] || [:put, :get, :post, :delete, :head]
235
+ @redirect = options[:redirect] || false
236
+ @parser = HttpClientParser.new
237
+ @ignore_data = options[:ignore_data]
238
+ end
239
+
240
+
241
+ # Builds a full request from the method, uri, req, and @cookies
242
+ # using the default @options and writes it to out (should be an IO).
243
+ def build_request(out, method, uri, req)
244
+ ops = @options.merge(req)
245
+ query = ops[:query]
246
+
247
+ # merge head differently since that's typically what they mean
248
+ head = req[:head] || {}
249
+ head = ops[:head].merge(head) if ops[:head]
250
+
251
+ # setup basic headers we always need
252
+ head[REQ_HOST] = encode_host(@host,@port)
253
+ head[REQ_CONTENT_LENGTH] = ops[:body] ? ops[:body].length : 0
254
+
255
+ # blast it out
256
+ out.write(HTTP_REQUEST_HEADER % [method, encode_query(uri,query)])
257
+ out.write(encode_headers(head))
258
+ if @cookies
259
+ out.write(encode_cookies(@cookies.merge(req[:cookies] || {})))
260
+ elsif req[:cookies]
261
+ out.write(encode_cookies(req[:cookies]))
262
+ end
263
+ out.write(CRLF)
264
+ end
265
+
266
+ # Does the read operations needed to parse a header with the @parser.
267
+ # A "header" in this case is either an HTTP header or a Chunked encoding
268
+ # header (since the @parser handles both).
269
+ def read_parsed_header
270
+ @parser.reset
271
+ resp = HttpResponse.new
272
+ data = @sock.read(CHUNK_SIZE, partial=true)
273
+ nread = @parser.execute(resp, data, 0)
274
+
275
+ while !@parser.finished?
276
+ data << @sock.read(CHUNK_SIZE, partial=true)
277
+ nread = @parser.execute(resp, data, nread)
278
+ end
279
+
280
+ return resp
281
+ end
282
+
283
+
284
+ # Used to process chunked headers and then read up their bodies.
285
+ def read_chunked_header
286
+ resp = read_parsed_header
287
+ @sock.push(resp.http_body)
288
+
289
+ if !resp.last_chunk?
290
+ resp.http_body = @sock.read(resp.chunk_size)
291
+
292
+ trail = @sock.read(2)
293
+ if trail != CRLF
294
+ raise HttpClientParserError.new("Chunk ended in #{trail.inspect} not #{CRLF.inspect}")
295
+ end
296
+ end
297
+
298
+ return resp
299
+ end
300
+
301
+
302
+ # Collects up a chunked body both collecting the body together *and*
303
+ # collecting the chunks into HttpResponse.raw_chunks[] for alternative
304
+ # analysis.
305
+ def read_chunked_body(header)
306
+ @sock.push(header.http_body)
307
+ header.http_body = ""
308
+ header.raw_chunks = []
309
+
310
+ while true
311
+ chunk = read_chunked_header
312
+ header.raw_chunks << chunk unless @ignore_data
313
+ if !chunk.last_chunk?
314
+ header.http_body << chunk.http_body unless @ignore_data
315
+ else
316
+ break # last chunk, done
317
+ end
318
+ end
319
+
320
+ header
321
+ end
322
+
323
+ # Reads the SET_COOKIE string out of resp and translates it into
324
+ # the @cookies store for this HttpClient.
325
+ def store_cookies(resp)
326
+ if @cookies and resp[SET_COOKIE]
327
+ cookies = query_parse(resp[SET_COOKIE], ';')
328
+ @cookies.merge! cookies
329
+ @cookies.delete "path"
330
+ end
331
+ end
332
+
333
+ # Reads an HTTP response from the given socket. It uses
334
+ # readpartial which only appeared in Ruby 1.8.4. The result
335
+ # is a fully formed HttpResponse object for you to play with.
336
+ #
337
+ # As with other methods in this class it doesn't stop any exceptions
338
+ # from reaching your code. It's for experts who want these exceptions
339
+ # so either write a wrapper, use net/http, or deal with it on your end.
340
+ def read_response
341
+ resp = HttpResponse.new
342
+
343
+ resp = read_parsed_header
344
+
345
+ if resp.chunked_encoding?
346
+ read_chunked_body(resp)
347
+ elsif resp[CONTENT_LENGTH]
348
+ needs = resp[CONTENT_LENGTH].to_i - resp.http_body.length
349
+ # Some requests can actually give a content length, and then not have content
350
+ # so we ignore HttpClientError exceptions and pray that's good enough
351
+ if @ignore_data
352
+ @sock.read(needs) if needs > 0 rescue HttpClientError
353
+ else
354
+ resp.http_body += @sock.read(needs) if needs > 0 rescue HttpClientError
355
+ end
356
+ else
357
+ while true
358
+ begin
359
+ if @ignore_data
360
+ @sock.read(CHUNK_SIZE, partial=true)
361
+ else
362
+ resp.http_body += @sock.read(CHUNK_SIZE, partial=true)
363
+ end
364
+ rescue HttpClientError
365
+ break # this is fine, they closed the socket then
366
+ end
367
+ end
368
+ end
369
+
370
+ store_cookies(resp) if @cookies
371
+ return resp
372
+ end
373
+
374
+ # Does the socket connect and then build_request, read_response
375
+ # calls finally returning the result.
376
+ def send_request(method, uri, req)
377
+ begin
378
+ @sock = PushBackIO.new(TCPSocket.new(@host, @port))
379
+
380
+ out = StringIO.new
381
+ build_request(out, method, uri, req)
382
+ body = req[:body] || "" unless method == :head or method == :get
383
+
384
+ @sock.write(out.string + body)
385
+ @sock.flush
386
+
387
+ return read_response
388
+ rescue Object
389
+ raise $!
390
+ ensure
391
+ if @sock
392
+ @sock.close
393
+ end
394
+ end
395
+ end
396
+
397
+
398
+ # Translates unknown function calls into PUT, GET, POST, DELETE, HEAD
399
+ # methods. The allowed HTTP methods allowed are restricted by the
400
+ # @allowed_methods attribute which you can set after construction or
401
+ # during construction with :allowed_methods => [:put, :get, ...]
402
+ def method_missing(symbol, *args)
403
+ if @allowed_methods.include? symbol
404
+ method = symbol.to_s.upcase
405
+ resp = send_request(method, args[0], args[1] || {})
406
+ resp = redirect(symbol, resp) if @redirect
407
+
408
+ return resp
409
+ else
410
+ raise HttpClientError.new("Invalid method: #{symbol}")
411
+ end
412
+ end
413
+
414
+ # Keeps doing requests until it doesn't receive a 3XX request.
415
+ def redirect(method, resp, *args)
416
+ @redirect.times do
417
+ break if resp.http_status.index("3") != 0
418
+
419
+ host = encode_host(@host,@port)
420
+ location = resp[LOCATION]
421
+
422
+ if location.index(host) == 0
423
+ # begins with the host so strip that off
424
+ location = location[host.length .. -1]
425
+ end
426
+
427
+ resp = self.send(method, location, *args)
428
+ end
429
+
430
+ return resp
431
+ end
432
+
433
+ # Clears out the cookies in use so far in order to get
434
+ # a clean slate.
435
+ def reset
436
+ @cookies.clear
437
+ end
438
+ end
439
+
440
+ end
441
+