rev 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0' unless defined? Rev::VERSION
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.0
7
- date: 2007-12-25 00:00:00 -07:00
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