arunthampi-evented_net 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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