fast_http 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/ext/http11_client/MANIFEST +0 -0
- data/ext/http11_client/Makefile +157 -0
- data/ext/http11_client/conftest.dSYM/Contents/Info.plist +25 -0
- data/ext/http11_client/conftest.dSYM/Contents/Resources/DWARF/conftest +0 -0
- data/ext/http11_client/ext_help.h +14 -0
- data/ext/http11_client/extconf.rb +6 -0
- data/ext/http11_client/http11_client.bundle +0 -0
- data/ext/http11_client/http11_client.c +302 -0
- data/ext/http11_client/http11_client.o +0 -0
- data/ext/http11_client/http11_parser.c +1052 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/http11_client/http11_parser.o +0 -0
- data/ext/http11_client/http11_parser.rl +173 -0
- data/ext/http11_client/mkmf.log +12 -0
- data/lib/fast_http.rb +3 -0
- data/lib/fast_http/client.rb +441 -0
- data/lib/fast_http/pushbackio.rb +90 -0
- data/test/test_httpparser.rb +47 -0
- metadata +84 -0
@@ -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
|
Binary file
|
@@ -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
|
+
|
data/lib/fast_http.rb
ADDED
@@ -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
|
+
|