rev 0.1.0 → 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/README +3 -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 +302 -0
- data/ext/http11_client/http11_parser.c +1052 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/rev/rev_watcher.c +16 -0
- data/lib/rev/buffered_io.rb +42 -25
- data/lib/rev/dns_resolver.rb +27 -26
- data/lib/rev/http_client.rb +419 -0
- data/lib/rev/socket.rb +3 -2
- data/lib/rev.rb +2 -1
- metadata +11 -2
@@ -0,0 +1,419 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# Includes portions originally Copyright (C)2005 Zed Shaw
|
4
|
+
# You can redistribute this under the terms of the Ruby license
|
5
|
+
# See file LICENSE for details
|
6
|
+
#++
|
7
|
+
|
8
|
+
require File.dirname(__FILE__) + '/../rev'
|
9
|
+
require File.dirname(__FILE__) + '/../http11_client'
|
10
|
+
|
11
|
+
module Rev
|
12
|
+
# A simple hash is returned for each request made by HttpClient with
|
13
|
+
# the headers that were given by the server for that request.
|
14
|
+
class HttpResponseHeader < Hash
|
15
|
+
# The reason returned in the http response ("OK","File not found",etc.)
|
16
|
+
attr_accessor :http_reason
|
17
|
+
|
18
|
+
# The HTTP version returned.
|
19
|
+
attr_accessor :http_version
|
20
|
+
|
21
|
+
# The status code (as a string!)
|
22
|
+
attr_accessor :http_status
|
23
|
+
|
24
|
+
# HTTP response status as an integer
|
25
|
+
def status
|
26
|
+
Integer(http_status) rescue nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Length of content as an integer, or nil if chunked/unspecified
|
30
|
+
def content_length
|
31
|
+
Integer(self[HttpClient::CONTENT_LENGTH]) rescue nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Is the transfer encoding chunked?
|
35
|
+
def chunked_encoding?
|
36
|
+
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class HttpChunkHeader < Hash
|
41
|
+
# When parsing chunked encodings this is set
|
42
|
+
attr_accessor :http_chunk_size
|
43
|
+
|
44
|
+
# Size of the chunk as an integer
|
45
|
+
def chunk_size
|
46
|
+
return @chunk_size unless @chunk_size.nil?
|
47
|
+
@chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Methods for building HTTP requests
|
52
|
+
module HttpEncoding
|
53
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
54
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
55
|
+
|
56
|
+
# Escapes a URI.
|
57
|
+
def escape(s)
|
58
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
59
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
60
|
+
}.tr(' ', '+')
|
61
|
+
end
|
62
|
+
|
63
|
+
# Unescapes a URI escaped string.
|
64
|
+
def unescape(s)
|
65
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
66
|
+
[$1.delete('%')].pack('H*')
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Map all header keys to a downcased string version
|
71
|
+
def munge_header_keys(head)
|
72
|
+
head.reduce({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
73
|
+
end
|
74
|
+
|
75
|
+
# HTTP is kind of retarded that you have to specify
|
76
|
+
# a Host header, but if you include port 80 then further
|
77
|
+
# redirects will tack on the :80 which is annoying.
|
78
|
+
def encode_host
|
79
|
+
remote_host + (remote_port.to_i != 80 ? ":#{remote_port}" : "")
|
80
|
+
end
|
81
|
+
|
82
|
+
def encode_request(method, uri, query)
|
83
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(uri, query)]
|
84
|
+
end
|
85
|
+
|
86
|
+
def encode_query(uri, query)
|
87
|
+
return uri unless query
|
88
|
+
uri + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
|
89
|
+
end
|
90
|
+
|
91
|
+
# URL encodes a single k=v parameter.
|
92
|
+
def encode_param(k, v)
|
93
|
+
escape(k) + "=" + escape(v)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Encode a field in an HTTP header
|
97
|
+
def encode_field(k, v)
|
98
|
+
FIELD_ENCODING % [k, v]
|
99
|
+
end
|
100
|
+
|
101
|
+
def encode_headers(head)
|
102
|
+
head.reduce('') do |result, (k, v)|
|
103
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
104
|
+
k = k.split('-').map(&:capitalize).join('-')
|
105
|
+
result << encode_field(k, v)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def encode_cookies(cookies)
|
110
|
+
cookies.reduce('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# HTTP client class implemented as a subclass of Rev::TCPSocket. Encodes
|
115
|
+
# requests and allows streaming consumption of the response. Response is
|
116
|
+
# parsed with a Ragel-generated whitelist parser which supports chunked
|
117
|
+
# HTTP encoding.
|
118
|
+
#
|
119
|
+
# == Example
|
120
|
+
#
|
121
|
+
# loop = Rev::Loop.default
|
122
|
+
# client = Rev::HttpClient.connect("www.google.com").attach
|
123
|
+
# client.get('/search', query: {q: 'foobar'})
|
124
|
+
# loop.run
|
125
|
+
#
|
126
|
+
class HttpClient < TCPSocket
|
127
|
+
include HttpEncoding
|
128
|
+
|
129
|
+
ALLOWED_METHODS=[:put, :get, :post, :delete, :head]
|
130
|
+
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
131
|
+
CONTENT_LENGTH="CONTENT_LENGTH"
|
132
|
+
SET_COOKIE="SET_COOKIE"
|
133
|
+
LOCATION="LOCATION"
|
134
|
+
HOST="HOST"
|
135
|
+
CRLF="\r\n"
|
136
|
+
|
137
|
+
# Connect to the given server, with port 80 as the default
|
138
|
+
def self.connect(addr, port = 80, *args)
|
139
|
+
super
|
140
|
+
end
|
141
|
+
|
142
|
+
def initialize(socket)
|
143
|
+
super
|
144
|
+
|
145
|
+
@parser = HttpClientParser.new
|
146
|
+
@parser_nbytes = 0
|
147
|
+
|
148
|
+
@state = :response_header
|
149
|
+
@data = ''
|
150
|
+
|
151
|
+
@response_header = HttpResponseHeader.new
|
152
|
+
@chunk_header = HttpChunkHeader.new
|
153
|
+
end
|
154
|
+
|
155
|
+
# Send an HTTP request and consume the response.
|
156
|
+
# Supports the following options:
|
157
|
+
#
|
158
|
+
# head: {Key: Value}
|
159
|
+
# Specify an HTTP header, e.g. {'Connection': 'close'}
|
160
|
+
#
|
161
|
+
# query: {Key: Value}
|
162
|
+
# Specify query string parameters (auto-escaped)
|
163
|
+
#
|
164
|
+
# cookies: {Key: Value}
|
165
|
+
# Specify hash of cookies (auto-escaped)
|
166
|
+
#
|
167
|
+
# body: String
|
168
|
+
# Specify the request body (you must encode it for now)
|
169
|
+
#
|
170
|
+
def request(method, uri, options = {})
|
171
|
+
raise RuntimeError, "request already sent" if @requested
|
172
|
+
|
173
|
+
@method, @uri, @options = method, uri, options
|
174
|
+
@requested = true
|
175
|
+
|
176
|
+
return unless @connected
|
177
|
+
send_request
|
178
|
+
end
|
179
|
+
|
180
|
+
# Requests can be made through method missing by invoking the HTTP method to use, i.e.:
|
181
|
+
#
|
182
|
+
# httpclient.get(path, options)
|
183
|
+
#
|
184
|
+
# Valid for: get, post, put, delete, head
|
185
|
+
#
|
186
|
+
# To use other HTTP methods, invoke the request method directly
|
187
|
+
#
|
188
|
+
def method_missing(method, *args)
|
189
|
+
raise NoMethodError, "method not supported" unless ALLOWED_METHODS.include? method.to_sym
|
190
|
+
request method, *args
|
191
|
+
end
|
192
|
+
|
193
|
+
# Called when response header has been received
|
194
|
+
def on_response_header(response_header)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Called when part of the body has been read
|
198
|
+
def on_body_data(data)
|
199
|
+
STDOUT.write data
|
200
|
+
STDOUT.flush
|
201
|
+
end
|
202
|
+
|
203
|
+
# Called when the request has completed
|
204
|
+
def on_request_complete
|
205
|
+
close
|
206
|
+
end
|
207
|
+
|
208
|
+
# Called when an error occurs during the request
|
209
|
+
def on_error(reason)
|
210
|
+
raise RuntimeError, reason
|
211
|
+
end
|
212
|
+
|
213
|
+
#########
|
214
|
+
protected
|
215
|
+
#########
|
216
|
+
|
217
|
+
#
|
218
|
+
# Rev callbacks
|
219
|
+
#
|
220
|
+
|
221
|
+
def on_connect
|
222
|
+
@connected = true
|
223
|
+
send_request if @method and @uri
|
224
|
+
end
|
225
|
+
|
226
|
+
def on_read(data)
|
227
|
+
until @state == :finished or @state == :invalid or data.empty?
|
228
|
+
@state, data = dispatch_data(@state, data)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
#
|
233
|
+
# Request sending
|
234
|
+
#
|
235
|
+
|
236
|
+
def send_request
|
237
|
+
send_request_header
|
238
|
+
send_request_body
|
239
|
+
end
|
240
|
+
|
241
|
+
def send_request_header
|
242
|
+
query = @options[:query]
|
243
|
+
head = @options[:head] ? munge_header_keys(@options[:head]) : {}
|
244
|
+
cookies = @options[:cookies]
|
245
|
+
body = @options[:body]
|
246
|
+
|
247
|
+
# Set the Host header if it hasn't been specified already
|
248
|
+
head['host'] ||= encode_host
|
249
|
+
|
250
|
+
# Set the Content-Length if it hasn't been specified already and a body was given
|
251
|
+
head['content-length'] ||= body ? body.length : 0
|
252
|
+
|
253
|
+
# Set the User-Agent if it hasn't been specified
|
254
|
+
head['user-agent'] ||= "Rev #{Rev::VERSION}"
|
255
|
+
|
256
|
+
# Default to Connection: close
|
257
|
+
head['connection'] ||= 'close'
|
258
|
+
|
259
|
+
# Build the request
|
260
|
+
request_header = encode_request(@method, @uri, query)
|
261
|
+
request_header << encode_headers(head)
|
262
|
+
request_header << encode_cookies(cookies) if cookies
|
263
|
+
request_header << CRLF
|
264
|
+
|
265
|
+
write request_header
|
266
|
+
end
|
267
|
+
|
268
|
+
def send_request_body
|
269
|
+
write @options[:body] if @options[:body]
|
270
|
+
end
|
271
|
+
|
272
|
+
#
|
273
|
+
# Response processing
|
274
|
+
#
|
275
|
+
|
276
|
+
def dispatch_data(state, data)
|
277
|
+
case state
|
278
|
+
when :response_header
|
279
|
+
parse_response_header(data)
|
280
|
+
when :chunk_header
|
281
|
+
parse_chunk_header(data)
|
282
|
+
when :chunk_body
|
283
|
+
process_chunk_body(data)
|
284
|
+
when :chunk_footer
|
285
|
+
process_chunk_footer(data)
|
286
|
+
when :response_footer
|
287
|
+
process_response_footer(data)
|
288
|
+
when :body
|
289
|
+
process_body(data)
|
290
|
+
else raise RuntimeError, "invalid state: #{@state}"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def parse_header(header, data)
|
295
|
+
@data << data
|
296
|
+
@parser_nbytes = @parser.execute(header, @data, @parser_nbytes)
|
297
|
+
return unless @parser.finished?
|
298
|
+
|
299
|
+
remainder = @data.slice(@parser_nbytes, @data.size)
|
300
|
+
@data = ''
|
301
|
+
@parser.reset
|
302
|
+
@parser_nbytes = 0
|
303
|
+
|
304
|
+
remainder
|
305
|
+
end
|
306
|
+
|
307
|
+
def parse_response_header(data)
|
308
|
+
data = parse_header(@response_header, data)
|
309
|
+
return :response_header, '' if data.nil?
|
310
|
+
|
311
|
+
unless @response_header.http_status and @response_header.http_reason
|
312
|
+
on_error "no HTTP response"
|
313
|
+
return :invalid
|
314
|
+
end
|
315
|
+
|
316
|
+
on_response_header(@response_header)
|
317
|
+
|
318
|
+
if @response_header.chunked_encoding?
|
319
|
+
return :chunk_header, data
|
320
|
+
else
|
321
|
+
@bytes_remaining = @response_header.content_length
|
322
|
+
return :body, data
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def parse_chunk_header(data)
|
327
|
+
data = parse_header(@chunk_header, data)
|
328
|
+
return :chunk_header, '' if data.nil?
|
329
|
+
|
330
|
+
@bytes_remaining = @chunk_header.chunk_size
|
331
|
+
@chunk_header = HttpChunkHeader.new
|
332
|
+
|
333
|
+
if @bytes_remaining > 0
|
334
|
+
return :chunk_body, data
|
335
|
+
else
|
336
|
+
@bytes_remaining = 2
|
337
|
+
return :response_footer, data
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def process_chunk_body(data)
|
342
|
+
if data.size < @bytes_remaining
|
343
|
+
@bytes_remaining -= data.size
|
344
|
+
on_body_data data
|
345
|
+
return :chunk_body, ''
|
346
|
+
end
|
347
|
+
|
348
|
+
on_body_data data.slice!(0, @bytes_remaining)
|
349
|
+
@bytes_remaining = 2
|
350
|
+
return :chunk_footer, data
|
351
|
+
end
|
352
|
+
|
353
|
+
def process_crlf(data)
|
354
|
+
@data << data.slice!(0, @bytes_remaining)
|
355
|
+
@bytes_remaining = 2 - @data.size
|
356
|
+
return unless @bytes_remaining == 0
|
357
|
+
|
358
|
+
matches_crlf = (@data == CRLF)
|
359
|
+
@data = ''
|
360
|
+
|
361
|
+
return matches_crlf, data
|
362
|
+
end
|
363
|
+
|
364
|
+
def process_chunk_footer(data)
|
365
|
+
result, data = process_crlf(data)
|
366
|
+
return :chunk_footer, '' if result.nil?
|
367
|
+
|
368
|
+
if result
|
369
|
+
return :chunk_header, data
|
370
|
+
else
|
371
|
+
on_error "non-CRLF chunk footer"
|
372
|
+
return :invalid
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def process_response_footer(data)
|
377
|
+
result, data = process_crlf(data)
|
378
|
+
return :response_footer, '' if result.nil?
|
379
|
+
if result
|
380
|
+
unless data.empty?
|
381
|
+
on_error "garbage at end of chunked response"
|
382
|
+
return :invalid
|
383
|
+
end
|
384
|
+
|
385
|
+
on_request_complete
|
386
|
+
return :finished
|
387
|
+
else
|
388
|
+
on_error "non-CRLF response footer"
|
389
|
+
return :invalid
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def process_body(data)
|
394
|
+
# FIXME the proper thing to do here is probably to keep reading until
|
395
|
+
# the socket closes, then assume that's the end of the body, provided
|
396
|
+
# the server has specified Connection: close
|
397
|
+
if @bytes_remaining.nil?
|
398
|
+
on_error "no content length specified"
|
399
|
+
return :invalid
|
400
|
+
end
|
401
|
+
|
402
|
+
if data.size < @bytes_remaining
|
403
|
+
@bytes_remaining -= data.size
|
404
|
+
on_body_data data
|
405
|
+
return :body, ''
|
406
|
+
end
|
407
|
+
|
408
|
+
on_body_data data.slice!(0, @bytes_remaining)
|
409
|
+
|
410
|
+
unless data.empty?
|
411
|
+
on_error "garbage at end of body"
|
412
|
+
return :invalid
|
413
|
+
end
|
414
|
+
|
415
|
+
on_request_complete
|
416
|
+
return :finished
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
data/lib/rev/socket.rb
CHANGED
@@ -55,10 +55,10 @@ module Rev
|
|
55
55
|
if connect_successful?
|
56
56
|
@rev_socket.instance_eval { @connector = nil }
|
57
57
|
@rev_socket.attach(evl)
|
58
|
-
@rev_socket.on_connect
|
58
|
+
@rev_socket.__send__(:on_connect)
|
59
59
|
else
|
60
60
|
@rev_socket.instance_eval { @failed = true }
|
61
|
-
@rev_socket.on_connect_failed
|
61
|
+
@rev_socket.__send__(:on_connect_failed)
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
@@ -97,6 +97,7 @@ module Rev
|
|
97
97
|
return allocate.instance_eval {
|
98
98
|
@remote_host, @remote_addr, @remote_port = addr, addr, port
|
99
99
|
@resolver = TCPConnectResolver.new(self, addr, port, *args)
|
100
|
+
self
|
100
101
|
}
|
101
102
|
end
|
102
103
|
|
data/lib/rev.rb
CHANGED
@@ -14,8 +14,9 @@ require File.dirname(__FILE__) + '/rev/buffered_io'
|
|
14
14
|
require File.dirname(__FILE__) + '/rev/dns_resolver'
|
15
15
|
require File.dirname(__FILE__) + '/rev/socket'
|
16
16
|
require File.dirname(__FILE__) + '/rev/server'
|
17
|
+
require File.dirname(__FILE__) + '/rev/http_client'
|
17
18
|
|
18
19
|
module Rev
|
19
|
-
Rev::VERSION = '0.1.
|
20
|
+
Rev::VERSION = '0.1.1' unless defined? Rev::VERSION
|
20
21
|
def self.version() VERSION end
|
21
22
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: rev
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.1.
|
7
|
-
date:
|
6
|
+
version: 0.1.1
|
7
|
+
date: 2008-01-01 00:00:00 -07:00
|
8
8
|
summary: Ruby 1.9 binding to the libev high performance event library
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -34,6 +34,7 @@ files:
|
|
34
34
|
- lib/rev
|
35
35
|
- lib/rev/buffered_io.rb
|
36
36
|
- lib/rev/dns_resolver.rb
|
37
|
+
- lib/rev/http_client.rb
|
37
38
|
- lib/rev/io_watcher.rb
|
38
39
|
- lib/rev/listener.rb
|
39
40
|
- lib/rev/loop.rb
|
@@ -42,11 +43,15 @@ files:
|
|
42
43
|
- lib/rev/timer_watcher.rb
|
43
44
|
- lib/rev/watcher.rb
|
44
45
|
- lib/rev.rb
|
46
|
+
- ext/http11_client/ext_help.h
|
47
|
+
- ext/http11_client/http11_parser.h
|
45
48
|
- ext/libev/ev.h
|
46
49
|
- ext/libev/ev_vars.h
|
47
50
|
- ext/libev/ev_wrap.h
|
48
51
|
- ext/rev/rev.h
|
49
52
|
- ext/rev/rev_watcher.h
|
53
|
+
- ext/http11_client/http11_client.c
|
54
|
+
- ext/http11_client/http11_parser.c
|
50
55
|
- ext/libev/ev.c
|
51
56
|
- ext/libev/ev_epoll.c
|
52
57
|
- ext/libev/ev_kqueue.c
|
@@ -59,6 +64,7 @@ files:
|
|
59
64
|
- ext/rev/rev_loop.c
|
60
65
|
- ext/rev/rev_timer_watcher.c
|
61
66
|
- ext/rev/rev_watcher.c
|
67
|
+
- ext/http11_client/extconf.rb
|
62
68
|
- ext/rev/extconf.rb
|
63
69
|
test_files:
|
64
70
|
- spec/rev_spec.rb
|
@@ -71,6 +77,8 @@ rdoc_options:
|
|
71
77
|
extra_rdoc_files:
|
72
78
|
- README
|
73
79
|
- LICENSE
|
80
|
+
- ext/http11_client/http11_client.c
|
81
|
+
- ext/http11_client/http11_parser.c
|
74
82
|
- ext/libev/ev.c
|
75
83
|
- ext/libev/ev_epoll.c
|
76
84
|
- ext/libev/ev_kqueue.c
|
@@ -86,6 +94,7 @@ extra_rdoc_files:
|
|
86
94
|
executables: []
|
87
95
|
|
88
96
|
extensions:
|
97
|
+
- ext/http11_client/extconf.rb
|
89
98
|
- ext/rev/extconf.rb
|
90
99
|
requirements: []
|
91
100
|
|