arunthampi-evented_net 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.rdoc +37 -0
- data/Rakefile +47 -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 +403 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/http11_client/http11_parser.rl +173 -0
- data/ext/rev/extconf.rb +6 -0
- data/ext/rev/rev_buffer.c +631 -0
- data/lib/evented_net.rb +18 -0
- data/lib/http.rb +6 -0
- data/lib/http/connection.rb +367 -0
- data/lib/http/get.rb +27 -0
- data/lib/http/post.rb +41 -0
- metadata +84 -0
data/lib/evented_net.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'eventmachine'
|
6
|
+
require 'net/http'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
# Native Extensions stolen from the 'rev' project:
|
10
|
+
# http://rev.rubyforge.org/svn/
|
11
|
+
require 'http11_client'
|
12
|
+
require 'rev_buffer'
|
13
|
+
# HTTP Classes
|
14
|
+
require 'http/connection'
|
15
|
+
require 'http/get'
|
16
|
+
require 'http/post'
|
17
|
+
# Main HTTP Module
|
18
|
+
require 'http'
|
data/lib/http.rb
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
module EventedNet
|
2
|
+
module HTTP
|
3
|
+
# A simple hash is returned for each request made by HttpClient with
|
4
|
+
# the headers that were given by the server for that request.
|
5
|
+
class HttpResponseHeader < Hash
|
6
|
+
# The reason returned in the http response ("OK","File not found",etc.)
|
7
|
+
attr_accessor :http_reason
|
8
|
+
|
9
|
+
# The HTTP version returned.
|
10
|
+
attr_accessor :http_version
|
11
|
+
|
12
|
+
# The status code (as a string!)
|
13
|
+
attr_accessor :http_status
|
14
|
+
|
15
|
+
# HTTP response status as an integer
|
16
|
+
def status
|
17
|
+
Integer(http_status) rescue nil
|
18
|
+
end
|
19
|
+
|
20
|
+
# Length of content as an integer, or nil if chunked/unspecified
|
21
|
+
def content_length
|
22
|
+
Integer(self[Connection::CONTENT_LENGTH]) rescue nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Is the transfer encoding chunked?
|
26
|
+
def chunked_encoding?
|
27
|
+
/chunked/i === self[Connection::TRANSFER_ENCODING]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class HttpChunkHeader < Hash
|
32
|
+
# When parsing chunked encodings this is set
|
33
|
+
attr_accessor :http_chunk_size
|
34
|
+
|
35
|
+
# Size of the chunk as an integer
|
36
|
+
def chunk_size
|
37
|
+
return @chunk_size unless @chunk_size.nil?
|
38
|
+
@chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Methods for building HTTP requests
|
43
|
+
module HttpEncoding
|
44
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
45
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
46
|
+
|
47
|
+
# Escapes a URI.
|
48
|
+
def escape(s)
|
49
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
50
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
51
|
+
}.tr(' ', '+')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Unescapes a URI escaped string.
|
55
|
+
def unescape(s)
|
56
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
57
|
+
[$1.delete('%')].pack('H*')
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Map all header keys to a downcased string version
|
62
|
+
def munge_header_keys(head)
|
63
|
+
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
64
|
+
end
|
65
|
+
|
66
|
+
# HTTP is kind of retarded that you have to specify
|
67
|
+
# a Host header, but if you include port 80 then further
|
68
|
+
# redirects will tack on the :80 which is annoying.
|
69
|
+
def encode_host
|
70
|
+
remote_host + (remote_port.to_i != 80 ? ":#{remote_port}" : "")
|
71
|
+
end
|
72
|
+
|
73
|
+
def encode_request(method, path, query)
|
74
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
|
75
|
+
end
|
76
|
+
|
77
|
+
def encode_query(path, query)
|
78
|
+
return path unless query
|
79
|
+
path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
|
80
|
+
end
|
81
|
+
|
82
|
+
# URL encodes a single k=v parameter.
|
83
|
+
def encode_param(k, v)
|
84
|
+
escape(k) + "=" + escape(v)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Encode a field in an HTTP header
|
88
|
+
def encode_field(k, v)
|
89
|
+
FIELD_ENCODING % [k, v]
|
90
|
+
end
|
91
|
+
|
92
|
+
def encode_headers(head)
|
93
|
+
head.inject('') do |result, (key, value)|
|
94
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
95
|
+
key = key.split('-').map { |k| k.capitalize }.join('-')
|
96
|
+
result << encode_field(key, value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def encode_cookies(cookies)
|
101
|
+
cookies.inject('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Connection < EventMachine::Connection
|
106
|
+
include EventMachine::Deferrable
|
107
|
+
include HttpEncoding
|
108
|
+
|
109
|
+
ALLOWED_METHODS=[:put, :get, :post, :delete, :head]
|
110
|
+
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
111
|
+
CONTENT_LENGTH="CONTENT_LENGTH"
|
112
|
+
SET_COOKIE="SET_COOKIE"
|
113
|
+
LOCATION="LOCATION"
|
114
|
+
HOST="HOST"
|
115
|
+
CRLF="\r\n"
|
116
|
+
|
117
|
+
class << self
|
118
|
+
def request(args = {})
|
119
|
+
args[:port] ||= 80
|
120
|
+
# According to the docs, we will get here AFTER post_init is called.
|
121
|
+
EventMachine.connect(args[:host], args[:port], self) do |c|
|
122
|
+
c.instance_eval { @args = args }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def remote_host
|
128
|
+
@args[:host]
|
129
|
+
end
|
130
|
+
|
131
|
+
def remote_port
|
132
|
+
@args[:port]
|
133
|
+
end
|
134
|
+
|
135
|
+
def post_init
|
136
|
+
@parser = Rev::HttpClientParser.new
|
137
|
+
@parser_nbytes = 0
|
138
|
+
@state = :response_header
|
139
|
+
@data = Rev::Buffer.new
|
140
|
+
@response_header = HttpResponseHeader.new
|
141
|
+
@response_body = ''
|
142
|
+
@chunk_header = HttpChunkHeader.new
|
143
|
+
end
|
144
|
+
|
145
|
+
def connection_completed
|
146
|
+
@connected = true
|
147
|
+
send_request(@args)
|
148
|
+
end
|
149
|
+
|
150
|
+
def send_request(args)
|
151
|
+
send_request_header(args)
|
152
|
+
send_request_body(args)
|
153
|
+
end
|
154
|
+
|
155
|
+
def send_request_header(args)
|
156
|
+
query = args[:query]
|
157
|
+
head = args[:head] ? munge_header_keys(args[:head]) : {}
|
158
|
+
cookies = args[:cookies]
|
159
|
+
body = args[:body]
|
160
|
+
path = args[:request]
|
161
|
+
|
162
|
+
path = "/#{path}" if path[0,1] != '/'
|
163
|
+
|
164
|
+
# Set the Host header if it hasn't been specified already
|
165
|
+
head['host'] ||= encode_host
|
166
|
+
# Set the Content-Length if it hasn't been specified already and a body was given
|
167
|
+
head['content-length'] ||= body ? body.length : 0
|
168
|
+
# Set the User-Agent if it hasn't been specified
|
169
|
+
head['user-agent'] ||= "EventedNet::HTTP::Connection"
|
170
|
+
# Default to Connection: close
|
171
|
+
head['connection'] ||= 'close'
|
172
|
+
# Build the request
|
173
|
+
request_header = encode_request(args[:method] || 'GET', path, query)
|
174
|
+
request_header << encode_headers(head)
|
175
|
+
request_header << encode_cookies(cookies) if cookies
|
176
|
+
request_header << CRLF
|
177
|
+
# Finally send it
|
178
|
+
send_data(request_header)
|
179
|
+
end
|
180
|
+
|
181
|
+
def send_request_body(args)
|
182
|
+
send_data(args[:body]) if args[:body]
|
183
|
+
end
|
184
|
+
|
185
|
+
def receive_data(data)
|
186
|
+
@data << data
|
187
|
+
dispatch
|
188
|
+
end
|
189
|
+
|
190
|
+
# Called when response header has been received
|
191
|
+
def on_response_header(response_header)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Called when part of the body has been read
|
195
|
+
def on_body_data(data)
|
196
|
+
@response_body = data
|
197
|
+
end
|
198
|
+
|
199
|
+
# Called when the request has completed
|
200
|
+
def on_request_complete
|
201
|
+
# Reset the state of the client
|
202
|
+
@state, @connected = :response_header, false
|
203
|
+
set_deferred_status :succeeded, {
|
204
|
+
:content => @response_body,
|
205
|
+
:headers => @response_header,
|
206
|
+
:status => @response_header.status
|
207
|
+
}
|
208
|
+
close_connection
|
209
|
+
end
|
210
|
+
|
211
|
+
# Called when an error occurs dispatching the request
|
212
|
+
def on_error(reason)
|
213
|
+
close_connection
|
214
|
+
raise RuntimeError, reason
|
215
|
+
end
|
216
|
+
|
217
|
+
def dispatch
|
218
|
+
while @connected and case @state
|
219
|
+
when :response_header
|
220
|
+
parse_response_header
|
221
|
+
when :chunk_header
|
222
|
+
parse_chunk_header
|
223
|
+
when :chunk_body
|
224
|
+
process_chunk_body
|
225
|
+
when :chunk_footer
|
226
|
+
process_chunk_footer
|
227
|
+
when :response_footer
|
228
|
+
process_response_footer
|
229
|
+
when :body
|
230
|
+
process_body
|
231
|
+
when :finished, :invalid
|
232
|
+
break
|
233
|
+
else raise RuntimeError, "Invalid state: #{@state}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def parse_header(header)
|
239
|
+
return false if @data.empty?
|
240
|
+
|
241
|
+
begin
|
242
|
+
@parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
|
243
|
+
rescue Rev::HttpClientParserError
|
244
|
+
on_error "Invalid HTTP format, parsing fails"
|
245
|
+
@state = :invalid
|
246
|
+
end
|
247
|
+
|
248
|
+
return false unless @parser.finished?
|
249
|
+
|
250
|
+
# Clear parsed data from the buffer
|
251
|
+
@data.read(@parser_nbytes)
|
252
|
+
@parser.reset
|
253
|
+
@parser_nbytes = 0
|
254
|
+
|
255
|
+
true
|
256
|
+
end
|
257
|
+
|
258
|
+
def parse_response_header
|
259
|
+
return false unless parse_header(@response_header)
|
260
|
+
|
261
|
+
unless @response_header.http_status and @response_header.http_reason
|
262
|
+
on_error "No HTTP response"
|
263
|
+
@state = :invalid
|
264
|
+
return false
|
265
|
+
end
|
266
|
+
|
267
|
+
on_response_header(@response_header)
|
268
|
+
|
269
|
+
if @response_header.chunked_encoding?
|
270
|
+
@state = :chunk_header
|
271
|
+
else
|
272
|
+
@state = :body
|
273
|
+
@bytes_remaining = @response_header.content_length
|
274
|
+
end
|
275
|
+
|
276
|
+
true
|
277
|
+
end
|
278
|
+
|
279
|
+
def parse_chunk_header
|
280
|
+
return false unless parse_header(@chunk_header)
|
281
|
+
|
282
|
+
@bytes_remaining = @chunk_header.chunk_size
|
283
|
+
@chunk_header = HttpChunkHeader.new
|
284
|
+
|
285
|
+
@state = @bytes_remaining > 0 ? :chunk_body : :response_footer
|
286
|
+
true
|
287
|
+
end
|
288
|
+
|
289
|
+
def process_chunk_body
|
290
|
+
if @data.size < @bytes_remaining
|
291
|
+
@bytes_remaining -= @data.size
|
292
|
+
on_body_data(@data.read)
|
293
|
+
return false
|
294
|
+
end
|
295
|
+
|
296
|
+
on_body_data(@data.read(@bytes_remaining))
|
297
|
+
@bytes_remaining = 0
|
298
|
+
|
299
|
+
@state = :chunk_footer
|
300
|
+
true
|
301
|
+
end
|
302
|
+
|
303
|
+
def process_chunk_footer
|
304
|
+
return false if @data.size < 2
|
305
|
+
|
306
|
+
if @data.read(2) == CRLF
|
307
|
+
@state = :chunk_header
|
308
|
+
else
|
309
|
+
on_error "Non-CRLF chunk footer"
|
310
|
+
@state = :invalid
|
311
|
+
end
|
312
|
+
|
313
|
+
true
|
314
|
+
end
|
315
|
+
|
316
|
+
def process_response_footer
|
317
|
+
return false if @data.size < 2
|
318
|
+
|
319
|
+
if @data.read(2) == CRLF
|
320
|
+
if @data.empty?
|
321
|
+
on_request_complete
|
322
|
+
@state = :finished
|
323
|
+
else
|
324
|
+
on_error "Garbage at end of chunked response"
|
325
|
+
@state = :invalid
|
326
|
+
end
|
327
|
+
else
|
328
|
+
on_error "Non-CRLF response footer"
|
329
|
+
@state = :invalid
|
330
|
+
end
|
331
|
+
|
332
|
+
false
|
333
|
+
end
|
334
|
+
|
335
|
+
def process_body
|
336
|
+
if @bytes_remaining.nil?
|
337
|
+
on_body_data(@data.read)
|
338
|
+
return false
|
339
|
+
end
|
340
|
+
|
341
|
+
if @bytes_remaining.zero?
|
342
|
+
on_request_complete
|
343
|
+
@state = :finished
|
344
|
+
return false
|
345
|
+
end
|
346
|
+
|
347
|
+
if @data.size < @bytes_remaining
|
348
|
+
@bytes_remaining -= @data.size
|
349
|
+
on_body_data(@data.read)
|
350
|
+
return false
|
351
|
+
end
|
352
|
+
|
353
|
+
on_body_data(@data.read(@bytes_remaining))
|
354
|
+
@bytes_remaining = 0
|
355
|
+
if @data.empty?
|
356
|
+
on_request_complete
|
357
|
+
@state = :finished
|
358
|
+
else
|
359
|
+
on_error "Garbage at end of body"
|
360
|
+
@state = :invalid
|
361
|
+
end
|
362
|
+
|
363
|
+
false
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
data/lib/http/get.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module EventedNet
|
2
|
+
module HTTP
|
3
|
+
module Get
|
4
|
+
def get(uri, opts = {})
|
5
|
+
unless uri.is_a?(URI) && (opts[:callback].is_a?(Proc) || opts[:callback].is_a?(Method)) && opts[:callback].arity == 2
|
6
|
+
raise ArgumentError, "uri must be a URI and opts[:callback] must be a Proc (or Method) which takes 2 args"
|
7
|
+
end
|
8
|
+
EM.reactor_running? ? evented_get(uri, opts) : synchronous_get(uri, opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
def synchronous_get(uri, opts = {})
|
12
|
+
r = Net::HTTP.get_response(uri)
|
13
|
+
opts[:callback].call(r.code, r.body)
|
14
|
+
end
|
15
|
+
|
16
|
+
def evented_get(uri, opts = {})
|
17
|
+
http = EventedNet::HTTP::Connection.request(
|
18
|
+
:host => uri.host, :port => uri.port,
|
19
|
+
:request => uri.path, :query => uri.query
|
20
|
+
)
|
21
|
+
# Assign the user generated callback, as the callback for
|
22
|
+
# EM::Protocols::HttpClient
|
23
|
+
http.callback { |r| opts[:callback].call(r[:status], r[:content]) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/http/post.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module EventedNet
|
2
|
+
module HTTP
|
3
|
+
module Post
|
4
|
+
def post(uri, opts = {})
|
5
|
+
unless uri.is_a?(URI) && (opts[:callback].is_a?(Proc) || opts[:callback].is_a?(Method)) && opts[:callback].arity == 2
|
6
|
+
raise ArgumentError, "uri must be a URI and opts[:callback] must be a Proc (or Method) which takes 2 args"
|
7
|
+
end
|
8
|
+
EM.reactor_running? ? evented_post(uri, opts) : synchronous_post(uri, opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def synchronous_post(uri, opts)
|
13
|
+
post_params = opts[:params] || {}
|
14
|
+
r = Net::HTTP.post_form(uri, post_params)
|
15
|
+
opts[:callback].call(r.code, r.body)
|
16
|
+
end
|
17
|
+
|
18
|
+
def evented_post(uri, opts)
|
19
|
+
post_params = opts[:params] || {}
|
20
|
+
post_params = post_params.collect{ |k,v| "#{urlencode(k.to_s)}=#{urlencode(v.to_s)}"}.join('&')
|
21
|
+
|
22
|
+
http = EventedNet::HTTP::Connection.request(
|
23
|
+
:host => uri.host, :port => uri.port,
|
24
|
+
:request => uri.path, :content => post_params,
|
25
|
+
:head =>
|
26
|
+
{
|
27
|
+
'Content-type' => opts[:content_type] || 'application/x-www-form-urlencoded'
|
28
|
+
},
|
29
|
+
:method => 'POST'
|
30
|
+
)
|
31
|
+
# Assign the user generated callback, as the callback for
|
32
|
+
# EM::Protocols::HttpClient
|
33
|
+
http.callback { |r| puts "#{r.inspect}"; opts[:callback].call(r[:status], r[:content]) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def urlencode(str)
|
37
|
+
str.gsub(/[^a-zA-Z0-9_\.\-]/n) {|s| sprintf('%%%02x', s[0]) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|