fast_http 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|