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.
@@ -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