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.
@@ -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,6 @@
1
+ module EventedNet
2
+ module HTTP
3
+ extend EventedNet::HTTP::Get
4
+ extend EventedNet::HTTP::Post
5
+ end
6
+ end
@@ -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