cool.io 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/.gitignore +25 -0
  2. data/CHANGES +199 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +4 -0
  5. data/Rakefile +98 -0
  6. data/VERSION +1 -0
  7. data/examples/echo_client.rb +38 -0
  8. data/examples/echo_server.rb +27 -0
  9. data/examples/google.rb +9 -0
  10. data/examples/httpclient.rb +38 -0
  11. data/ext/cool.io/.gitignore +5 -0
  12. data/ext/cool.io/cool.io.h +58 -0
  13. data/ext/cool.io/cool.io_ext.c +25 -0
  14. data/ext/cool.io/ev_wrap.h +8 -0
  15. data/ext/cool.io/extconf.rb +69 -0
  16. data/ext/cool.io/iowatcher.c +189 -0
  17. data/ext/cool.io/libev.c +8 -0
  18. data/ext/cool.io/loop.c +303 -0
  19. data/ext/cool.io/stat_watcher.c +191 -0
  20. data/ext/cool.io/timer_watcher.c +219 -0
  21. data/ext/cool.io/utils.c +122 -0
  22. data/ext/cool.io/watcher.c +264 -0
  23. data/ext/cool.io/watcher.h +71 -0
  24. data/ext/http11_client/.gitignore +5 -0
  25. data/ext/http11_client/ext_help.h +14 -0
  26. data/ext/http11_client/extconf.rb +6 -0
  27. data/ext/http11_client/http11_client.c +300 -0
  28. data/ext/http11_client/http11_parser.c +403 -0
  29. data/ext/http11_client/http11_parser.h +48 -0
  30. data/ext/http11_client/http11_parser.rl +173 -0
  31. data/ext/libev/Changes +364 -0
  32. data/ext/libev/LICENSE +36 -0
  33. data/ext/libev/README +58 -0
  34. data/ext/libev/README.embed +3 -0
  35. data/ext/libev/ev.c +3867 -0
  36. data/ext/libev/ev.h +826 -0
  37. data/ext/libev/ev_epoll.c +234 -0
  38. data/ext/libev/ev_kqueue.c +198 -0
  39. data/ext/libev/ev_poll.c +148 -0
  40. data/ext/libev/ev_port.c +164 -0
  41. data/ext/libev/ev_select.c +307 -0
  42. data/ext/libev/ev_vars.h +197 -0
  43. data/ext/libev/ev_win32.c +153 -0
  44. data/ext/libev/ev_wrap.h +186 -0
  45. data/ext/libev/test_libev_win32.c +123 -0
  46. data/ext/libev/update_ev_wrap +19 -0
  47. data/lib/.gitignore +2 -0
  48. data/lib/cool.io.rb +30 -0
  49. data/lib/cool.io/async_watcher.rb +43 -0
  50. data/lib/cool.io/dns_resolver.rb +220 -0
  51. data/lib/cool.io/eventmachine.rb +234 -0
  52. data/lib/cool.io/http_client.rb +419 -0
  53. data/lib/cool.io/io.rb +174 -0
  54. data/lib/cool.io/iowatcher.rb +17 -0
  55. data/lib/cool.io/listener.rb +93 -0
  56. data/lib/cool.io/loop.rb +130 -0
  57. data/lib/cool.io/meta.rb +49 -0
  58. data/lib/cool.io/server.rb +74 -0
  59. data/lib/cool.io/socket.rb +224 -0
  60. data/lib/cool.io/timer_watcher.rb +17 -0
  61. data/lib/coolio.rb +2 -0
  62. data/lib/rev.rb +4 -0
  63. data/spec/async_watcher_spec.rb +57 -0
  64. data/spec/possible_tests/schedules_other_threads.rb +48 -0
  65. data/spec/possible_tests/test_on_resolve_failed.rb +9 -0
  66. data/spec/possible_tests/test_resolves.rb +27 -0
  67. data/spec/possible_tests/test_write_during_resolve.rb +27 -0
  68. data/spec/possible_tests/works_straight.rb +71 -0
  69. data/spec/spec_helper.rb +5 -0
  70. data/spec/timer_watcher_spec.rb +55 -0
  71. data/spec/unix_listener_spec.rb +25 -0
  72. data/spec/unix_server_spec.rb +25 -0
  73. metadata +184 -0
@@ -0,0 +1,419 @@
1
+ #--
2
+ # Copyright (C)2007-10 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 'http11_client'
9
+
10
+ module Coolio
11
+ # A simple hash is returned for each request made by HttpClient with
12
+ # the headers that were given by the server for that request.
13
+ class HttpResponseHeader < Hash
14
+ # The reason returned in the http response ("OK","File not found",etc.)
15
+ attr_accessor :http_reason
16
+
17
+ # The HTTP version returned.
18
+ attr_accessor :http_version
19
+
20
+ # The status code (as a string!)
21
+ attr_accessor :http_status
22
+
23
+ # HTTP response status as an integer
24
+ def status
25
+ Integer(http_status) rescue nil
26
+ end
27
+
28
+ # Length of content as an integer, or nil if chunked/unspecified
29
+ def content_length
30
+ Integer(self[HttpClient::CONTENT_LENGTH]) rescue nil
31
+ end
32
+
33
+ # Is the transfer encoding chunked?
34
+ def chunked_encoding?
35
+ /chunked/i === self[HttpClient::TRANSFER_ENCODING]
36
+ end
37
+ end
38
+
39
+ class HttpChunkHeader < Hash
40
+ # When parsing chunked encodings this is set
41
+ attr_accessor :http_chunk_size
42
+
43
+ # Size of the chunk as an integer
44
+ def chunk_size
45
+ return @chunk_size unless @chunk_size.nil?
46
+ @chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
47
+ end
48
+ end
49
+
50
+ # Methods for building HTTP requests
51
+ module HttpEncoding
52
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
53
+ FIELD_ENCODING = "%s: %s\r\n"
54
+
55
+ # Escapes a URI.
56
+ def escape(s)
57
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
58
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
59
+ }.tr(' ', '+')
60
+ end
61
+
62
+ # Unescapes a URI escaped string.
63
+ def unescape(s)
64
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
65
+ [$1.delete('%')].pack('H*')
66
+ }
67
+ end
68
+
69
+ # Map all header keys to a downcased string version
70
+ def munge_header_keys(head)
71
+ head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
72
+ end
73
+
74
+ # HTTP is kind of retarded that you have to specify
75
+ # a Host header, but if you include port 80 then further
76
+ # redirects will tack on the :80 which is annoying.
77
+ def encode_host
78
+ remote_host + (remote_port.to_i != 80 ? ":#{remote_port}" : "")
79
+ end
80
+
81
+ def encode_request(method, path, query)
82
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
83
+ end
84
+
85
+ def encode_query(path, query)
86
+ return path unless query
87
+ path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
88
+ end
89
+
90
+ # URL encodes a single k=v parameter.
91
+ def encode_param(k, v)
92
+ escape(k) + "=" + escape(v)
93
+ end
94
+
95
+ # Encode a field in an HTTP header
96
+ def encode_field(k, v)
97
+ FIELD_ENCODING % [k, v]
98
+ end
99
+
100
+ def encode_headers(head)
101
+ head.inject('') do |result, (key, value)|
102
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
103
+ key = key.split('-').map { |k| k.capitalize }.join('-')
104
+ result << encode_field(key, value)
105
+ end
106
+ end
107
+
108
+ def encode_cookies(cookies)
109
+ cookies.inject('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
110
+ end
111
+ end
112
+
113
+ # HTTP client class implemented as a subclass of Coolio::TCPSocket. Encodes
114
+ # requests and allows streaming consumption of the response. Response is
115
+ # parsed with a Ragel-generated whitelist parser which supports chunked
116
+ # HTTP encoding.
117
+ #
118
+ # == Example
119
+ #
120
+ # loop = Coolio::Loop.default
121
+ # client = Coolio::HttpClient.connect("www.google.com").attach
122
+ # client.get('/search', query: {q: 'foobar'})
123
+ # loop.run
124
+ #
125
+ class HttpClient < TCPSocket
126
+ include HttpEncoding
127
+
128
+ ALLOWED_METHODS=[:put, :get, :post, :delete, :head]
129
+ TRANSFER_ENCODING="TRANSFER_ENCODING"
130
+ CONTENT_LENGTH="CONTENT_LENGTH"
131
+ SET_COOKIE="SET_COOKIE"
132
+ LOCATION="LOCATION"
133
+ HOST="HOST"
134
+ CRLF="\r\n"
135
+
136
+ # Connect to the given server, with port 80 as the default
137
+ def self.connect(addr, port = 80, *args)
138
+ super
139
+ end
140
+
141
+ def initialize(socket)
142
+ super
143
+
144
+ @parser = HttpClientParser.new
145
+ @parser_nbytes = 0
146
+
147
+ @state = :response_header
148
+ @data = ::IO::Buffer.new
149
+
150
+ @response_header = HttpResponseHeader.new
151
+ @chunk_header = HttpChunkHeader.new
152
+ end
153
+
154
+ # Send an HTTP request and consume the response.
155
+ # Supports the following options:
156
+ #
157
+ # head: {Key: Value}
158
+ # Specify an HTTP header, e.g. {'Connection': 'close'}
159
+ #
160
+ # query: {Key: Value}
161
+ # Specify query string parameters (auto-escaped)
162
+ #
163
+ # cookies: {Key: Value}
164
+ # Specify hash of cookies (auto-escaped)
165
+ #
166
+ # body: String
167
+ # Specify the request body (you must encode it for now)
168
+ #
169
+ def request(method, path, options = {})
170
+ raise ArgumentError, "invalid request path" unless /^\// === path
171
+ raise RuntimeError, "request already sent" if @requested
172
+
173
+ @method, @path, @options = method, path, options
174
+ @requested = true
175
+
176
+ return unless @connected
177
+ send_request
178
+ end
179
+
180
+ # Enable the HttpClient if it has been disabled
181
+ def enable
182
+ super
183
+ dispatch unless @data.empty?
184
+ end
185
+
186
+ # Called when response header has been received
187
+ def on_response_header(response_header)
188
+ end
189
+
190
+ # Called when part of the body has been read
191
+ def on_body_data(data)
192
+ STDOUT.write data
193
+ STDOUT.flush
194
+ end
195
+
196
+ # Called when the request has completed
197
+ def on_request_complete
198
+ close
199
+ end
200
+
201
+ # Called when an error occurs dispatching the request
202
+ def on_error(reason)
203
+ close
204
+ raise RuntimeError, reason
205
+ end
206
+
207
+ #########
208
+ protected
209
+ #########
210
+
211
+ #
212
+ # Coolio callbacks
213
+ #
214
+
215
+ def on_connect
216
+ @connected = true
217
+ send_request if @method and @path
218
+ end
219
+
220
+ def on_read(data)
221
+ @data << data
222
+ dispatch
223
+ end
224
+
225
+ #
226
+ # Request sending
227
+ #
228
+
229
+ def send_request
230
+ send_request_header
231
+ send_request_body
232
+ end
233
+
234
+ def send_request_header
235
+ query = @options[:query]
236
+ head = @options[:head] ? munge_header_keys(@options[:head]) : {}
237
+ cookies = @options[:cookies]
238
+ body = @options[:body]
239
+
240
+ # Set the Host header if it hasn't been specified already
241
+ head['host'] ||= encode_host
242
+
243
+ # Set the Content-Length if it hasn't been specified already and a body was given
244
+ head['content-length'] ||= body ? body.length : 0
245
+
246
+ # Set the User-Agent if it hasn't been specified
247
+ head['user-agent'] ||= "Coolio #{Coolio::VERSION}"
248
+
249
+ # Default to Connection: close
250
+ head['connection'] ||= 'close'
251
+
252
+ # Build the request
253
+ request_header = encode_request(@method, @path, query)
254
+ request_header << encode_headers(head)
255
+ request_header << encode_cookies(cookies) if cookies
256
+ request_header << CRLF
257
+
258
+ write request_header
259
+ end
260
+
261
+ def send_request_body
262
+ write @options[:body] if @options[:body]
263
+ end
264
+
265
+ #
266
+ # Response processing
267
+ #
268
+
269
+ def dispatch
270
+ while enabled? and case @state
271
+ when :response_header
272
+ parse_response_header
273
+ when :chunk_header
274
+ parse_chunk_header
275
+ when :chunk_body
276
+ process_chunk_body
277
+ when :chunk_footer
278
+ process_chunk_footer
279
+ when :response_footer
280
+ process_response_footer
281
+ when :body
282
+ process_body
283
+ when :finished, :invalid
284
+ break
285
+ else raise RuntimeError, "invalid state: #{@state}"
286
+ end
287
+ end
288
+ end
289
+
290
+ def parse_header(header)
291
+ return false if @data.empty?
292
+
293
+ begin
294
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
295
+ rescue Coolio::HttpClientParserError
296
+ on_error "invalid HTTP format, parsing fails"
297
+ @state = :invalid
298
+ end
299
+
300
+ return false unless @parser.finished?
301
+
302
+ # Clear parsed data from the buffer
303
+ @data.read(@parser_nbytes)
304
+ @parser.reset
305
+ @parser_nbytes = 0
306
+
307
+ true
308
+ end
309
+
310
+ def parse_response_header
311
+ return false unless parse_header(@response_header)
312
+
313
+ unless @response_header.http_status and @response_header.http_reason
314
+ on_error "no HTTP response"
315
+ @state = :invalid
316
+ return false
317
+ end
318
+
319
+ on_response_header(@response_header)
320
+
321
+ if @response_header.chunked_encoding?
322
+ @state = :chunk_header
323
+ else
324
+ @state = :body
325
+ @bytes_remaining = @response_header.content_length
326
+ end
327
+
328
+ true
329
+ end
330
+
331
+ def parse_chunk_header
332
+ return false unless parse_header(@chunk_header)
333
+
334
+ @bytes_remaining = @chunk_header.chunk_size
335
+ @chunk_header = HttpChunkHeader.new
336
+
337
+ @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
338
+ true
339
+ end
340
+
341
+ def process_chunk_body
342
+ if @data.size < @bytes_remaining
343
+ @bytes_remaining -= @data.size
344
+ on_body_data @data.read
345
+ return false
346
+ end
347
+
348
+ on_body_data @data.read(@bytes_remaining)
349
+ @bytes_remaining = 0
350
+
351
+ @state = :chunk_footer
352
+ true
353
+ end
354
+
355
+ def process_chunk_footer
356
+ return false if @data.size < 2
357
+
358
+ if @data.read(2) == CRLF
359
+ @state = :chunk_header
360
+ else
361
+ on_error "non-CRLF chunk footer"
362
+ @state = :invalid
363
+ end
364
+
365
+ true
366
+ end
367
+
368
+ def process_response_footer
369
+ return false if @data.size < 2
370
+
371
+ if @data.read(2) == CRLF
372
+ if @data.empty?
373
+ on_request_complete
374
+ @state = :finished
375
+ else
376
+ on_error "garbage at end of chunked response"
377
+ @state = :invalid
378
+ end
379
+ else
380
+ on_error "non-CRLF response footer"
381
+ @state = :invalid
382
+ end
383
+
384
+ false
385
+ end
386
+
387
+ def process_body
388
+ if @bytes_remaining.nil?
389
+ on_body_data @data.read
390
+ return false
391
+ end
392
+
393
+ if @bytes_remaining.zero?
394
+ on_request_complete
395
+ @state = :finished
396
+ return false
397
+ end
398
+
399
+ if @data.size < @bytes_remaining
400
+ @bytes_remaining -= @data.size
401
+ on_body_data @data.read
402
+ return false
403
+ end
404
+
405
+ on_body_data @data.read(@bytes_remaining)
406
+ @bytes_remaining = 0
407
+
408
+ if @data.empty?
409
+ on_request_complete
410
+ @state = :finished
411
+ else
412
+ on_error "garbage at end of body"
413
+ @state = :invalid
414
+ end
415
+
416
+ false
417
+ end
418
+ end
419
+ end
data/lib/cool.io/io.rb ADDED
@@ -0,0 +1,174 @@
1
+ #--
2
+ # Copyright (C)2007-10 Tony Arcieri
3
+ # You can redistribute this under the terms of the Ruby license
4
+ # See file LICENSE for details
5
+ #++
6
+
7
+ module Coolio
8
+ # A buffered I/O class witch fits into the Coolio Watcher framework.
9
+ # It provides both an observer which reads data as it's received
10
+ # from the wire and a buffered write watcher which stores data and writes
11
+ # it out each time the socket becomes writable.
12
+ #
13
+ # This class is primarily meant as a base class for other streams
14
+ # which need non-blocking writing, and is used to implement Coolio's
15
+ # Socket class and its associated subclasses.
16
+ class IO
17
+ extend Meta
18
+
19
+ # Maximum number of bytes to consume at once
20
+ INPUT_SIZE = 16384
21
+
22
+ def initialize(io)
23
+ @_io = io
24
+ @_write_buffer ||= ::IO::Buffer.new
25
+ @_read_watcher = Watcher.new(io, self, :r)
26
+ @_write_watcher = Watcher.new(io, self, :w)
27
+ end
28
+
29
+ #
30
+ # Watcher methods, delegated to @_read_watcher
31
+ #
32
+
33
+ # Attach to the event loop
34
+ def attach(loop); @_read_watcher.attach loop; schedule_write if !@_write_buffer.empty?; self; end
35
+
36
+ # Detach from the event loop
37
+ def detach; @_read_watcher.detach; self; end # TODO should these detect write buffers, as well?
38
+
39
+ # Enable the watcher
40
+ def enable; @_read_watcher.enable; self; end
41
+
42
+ # Disable the watcher
43
+ def disable; @_read_watcher.disable; self; end
44
+
45
+ # Is the watcher attached?
46
+ def attached?; @_read_watcher.attached?; end
47
+
48
+ # Is the watcher enabled?
49
+ def enabled?; @_read_watcher.enabled?; end
50
+
51
+ # Obtain the event loop associated with this object
52
+ def evloop; @_read_watcher.evloop; end
53
+
54
+ #
55
+ # Callbacks for asynchronous events
56
+ #
57
+
58
+ # Called whenever the IO object receives data
59
+ def on_read(data); end
60
+ event_callback :on_read
61
+
62
+ # Called whenever a write completes and the output buffer is empty
63
+ def on_write_complete; end
64
+ event_callback :on_write_complete
65
+
66
+ # Called whenever the IO object hits EOF
67
+ def on_close; end
68
+ event_callback :on_close
69
+
70
+ #
71
+ # Write interface
72
+ #
73
+
74
+ # Write data in a buffered, non-blocking manner
75
+ def write(data)
76
+ @_write_buffer << data
77
+ schedule_write
78
+ data.size
79
+ end
80
+
81
+ # Number of bytes are currently in the output buffer
82
+ def output_buffer_size
83
+ @_write_buffer.size
84
+ end
85
+
86
+ # Close the IO stream
87
+ def close
88
+ detach if attached?
89
+ detach_write_watcher
90
+ @_io.close unless @_io.closed?
91
+
92
+ on_close
93
+ nil
94
+ end
95
+
96
+ # Is the IO object closed?
97
+ def closed?
98
+ @_io.nil? or @_io.closed?
99
+ end
100
+
101
+ #########
102
+ protected
103
+ #########
104
+
105
+ # Read from the input buffer and dispatch to on_read
106
+ def on_readable
107
+ begin
108
+ on_read @_io.read_nonblock(INPUT_SIZE)
109
+ rescue Errno::EAGAIN, Errno::EINTR
110
+ return
111
+
112
+ # SystemCallError catches Errno::ECONNRESET amongst others.
113
+ rescue SystemCallError, EOFError, IOError, SocketError
114
+ close
115
+ end
116
+ end
117
+
118
+ # Write the contents of the output buffer
119
+ def on_writable
120
+ begin
121
+ @_write_buffer.write_to(@_io)
122
+ rescue Errno::EINTR
123
+ return
124
+
125
+ # SystemCallError catches Errno::EPIPE & Errno::ECONNRESET amongst others.
126
+ rescue SystemCallError, IOError, SocketError
127
+ return close
128
+ end
129
+
130
+ if @_write_buffer.empty?
131
+ disable_write_watcher
132
+ on_write_complete
133
+ end
134
+ end
135
+
136
+ # Schedule a write to be performed when the IO object becomes writable
137
+ def schedule_write
138
+ return unless @_io # this would mean 'we are still pre DNS here'
139
+ return unless attached? # this would mean 'currently unattached' -- ie still pre DNS, or just plain not attached, which is ok
140
+ begin
141
+ enable_write_watcher
142
+ rescue IOError
143
+ end
144
+ end
145
+
146
+ def enable_write_watcher
147
+ if @_write_watcher.attached?
148
+ @_write_watcher.enable unless @_write_watcher.enabled?
149
+ else
150
+ @_write_watcher.attach(evloop)
151
+ end
152
+ end
153
+
154
+ def disable_write_watcher
155
+ @_write_watcher.disable if @_write_watcher and @_write_watcher.enabled?
156
+ end
157
+
158
+ def detach_write_watcher
159
+ @_write_watcher.detach if @_write_watcher and @_write_watcher.attached?
160
+ end
161
+
162
+ # Internal class implementing watchers used by Coolio::IO
163
+ class Watcher < IOWatcher
164
+ def initialize(ruby_io, coolio_io, flags)
165
+ @coolio_io = coolio_io
166
+ super(ruby_io, flags)
167
+ end
168
+
169
+ # Configure IOWatcher event callbacks to call the method passed to #initialize
170
+ def on_readable; @coolio_io.__send__(:on_readable); end
171
+ def on_writable; @coolio_io.__send__(:on_writable); end
172
+ end
173
+ end
174
+ end