cool.io 1.2.0-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/.gitignore +26 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +4 -0
  4. data/CHANGES.md +176 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +172 -0
  8. data/Rakefile +81 -0
  9. data/cool.io.gemspec +28 -0
  10. data/examples/dslified_echo_client.rb +34 -0
  11. data/examples/dslified_echo_server.rb +24 -0
  12. data/examples/echo_client.rb +38 -0
  13. data/examples/echo_server.rb +27 -0
  14. data/examples/google.rb +9 -0
  15. data/examples/httpclient.rb +38 -0
  16. data/ext/cool.io/.gitignore +5 -0
  17. data/ext/cool.io/cool.io.h +58 -0
  18. data/ext/cool.io/cool.io_ext.c +25 -0
  19. data/ext/cool.io/ev_wrap.h +8 -0
  20. data/ext/cool.io/extconf.rb +73 -0
  21. data/ext/cool.io/iowatcher.c +189 -0
  22. data/ext/cool.io/libev.c +8 -0
  23. data/ext/cool.io/loop.c +301 -0
  24. data/ext/cool.io/stat_watcher.c +269 -0
  25. data/ext/cool.io/timer_watcher.c +219 -0
  26. data/ext/cool.io/utils.c +122 -0
  27. data/ext/cool.io/watcher.c +264 -0
  28. data/ext/cool.io/watcher.h +71 -0
  29. data/ext/http11_client/.gitignore +5 -0
  30. data/ext/http11_client/LICENSE +31 -0
  31. data/ext/http11_client/ext_help.h +14 -0
  32. data/ext/http11_client/extconf.rb +6 -0
  33. data/ext/http11_client/http11_client.c +300 -0
  34. data/ext/http11_client/http11_parser.c +403 -0
  35. data/ext/http11_client/http11_parser.h +48 -0
  36. data/ext/http11_client/http11_parser.rl +173 -0
  37. data/ext/iobuffer/extconf.rb +9 -0
  38. data/ext/iobuffer/iobuffer.c +765 -0
  39. data/ext/libev/Changes +388 -0
  40. data/ext/libev/LICENSE +36 -0
  41. data/ext/libev/README +58 -0
  42. data/ext/libev/README.embed +3 -0
  43. data/ext/libev/ev.c +4803 -0
  44. data/ext/libev/ev.h +845 -0
  45. data/ext/libev/ev_epoll.c +279 -0
  46. data/ext/libev/ev_kqueue.c +214 -0
  47. data/ext/libev/ev_poll.c +148 -0
  48. data/ext/libev/ev_port.c +185 -0
  49. data/ext/libev/ev_select.c +314 -0
  50. data/ext/libev/ev_vars.h +203 -0
  51. data/ext/libev/ev_win32.c +163 -0
  52. data/ext/libev/ev_wrap.h +200 -0
  53. data/ext/libev/test_libev_win32.c +123 -0
  54. data/lib/.gitignore +2 -0
  55. data/lib/cool.io.rb +32 -0
  56. data/lib/cool.io/async_watcher.rb +43 -0
  57. data/lib/cool.io/custom_require.rb +9 -0
  58. data/lib/cool.io/dns_resolver.rb +225 -0
  59. data/lib/cool.io/dsl.rb +135 -0
  60. data/lib/cool.io/eventmachine.rb +234 -0
  61. data/lib/cool.io/http_client.rb +427 -0
  62. data/lib/cool.io/io.rb +174 -0
  63. data/lib/cool.io/iowatcher.rb +17 -0
  64. data/lib/cool.io/listener.rb +93 -0
  65. data/lib/cool.io/loop.rb +130 -0
  66. data/lib/cool.io/meta.rb +49 -0
  67. data/lib/cool.io/server.rb +74 -0
  68. data/lib/cool.io/socket.rb +230 -0
  69. data/lib/cool.io/timer_watcher.rb +17 -0
  70. data/lib/cool.io/version.rb +5 -0
  71. data/lib/coolio.rb +2 -0
  72. data/spec/async_watcher_spec.rb +57 -0
  73. data/spec/dns_spec.rb +39 -0
  74. data/spec/spec_helper.rb +12 -0
  75. data/spec/stat_watcher_spec.rb +77 -0
  76. data/spec/timer_watcher_spec.rb +55 -0
  77. data/spec/unix_listener_spec.rb +25 -0
  78. data/spec/unix_server_spec.rb +25 -0
  79. metadata +200 -0
@@ -0,0 +1,234 @@
1
+ #--
2
+ # Copyright (C)2007-10 Tony Arcieri, Roger Pack
3
+ # You can redistribute this under the terms of the Ruby license
4
+ # See file LICENSE for details
5
+ #++
6
+
7
+ require 'cool.io'
8
+
9
+ # EventMachine emulation for Cool.io:
10
+ #
11
+ # require 'coolio/eventmachine'
12
+ #
13
+ # Drawbacks: slightly slower than EM.
14
+ # Benefits: timers are more accurate using libev than using EM
15
+ # TODO: some things like connection timeouts aren't implemented yet
16
+ # DONE: timers and normal socket functions are implemented.
17
+ module EventMachine
18
+ class << self
19
+ # Start the Reactor loop
20
+ def run
21
+ yield if block_given?
22
+ Coolio::Loop.default.run
23
+ end
24
+
25
+ # Stop the Reactor loop
26
+ def stop_event_loop
27
+ Coolio::Loop.default.stop
28
+ end
29
+
30
+ class OneShotEMTimer < Coolio::TimerWatcher
31
+ def setup(proc)
32
+ @proc = proc
33
+ end
34
+
35
+ def on_timer
36
+ @proc.call
37
+ end
38
+ end
39
+
40
+ # ltodo: use Coolio's PeriodicTimer to wrap EM's two similar to it
41
+ # todo: close all connections on 'stop', I believe
42
+
43
+ def add_timer(interval, proc = nil, &block)
44
+ block ||= proc
45
+ t = OneShotEMTimer.new(interval, false) # non repeating
46
+ t.setup(block)
47
+
48
+ # fire 'er off ltodo: do we keep track of these timers in memory?
49
+ t.attach(Coolio::Loop.default)
50
+ t
51
+ end
52
+
53
+ def cancel_timer(t)
54
+ # guess there's a case where EM you can say 'cancel' but it's already fired?
55
+ # kind of odd but it happens
56
+ t.detach if t.attached?
57
+ end
58
+
59
+ def set_comm_inactivity_timeout(*args); end # TODO
60
+
61
+ # Make an outgoing connection
62
+ def connect(addr, port, handler = Connection, *args, &block)
63
+ block = args.pop if Proc === args[-1]
64
+
65
+ # make sure we're a 'real' class here
66
+ klass = if (handler and handler.is_a?(Class))
67
+ handler
68
+ else
69
+ Class.new( Connection ) {handler and include handler}
70
+ end
71
+
72
+ wrapped_child = CallsBackToEM.connect(addr, port, *args) # ltodo: args? what? they're used? also TODOC TODO FIX
73
+ conn = klass.new(wrapped_child) # ltodo [?] addr, port, *args)
74
+ wrapped_child.attach(Coolio::Loop.default) # necessary
75
+ conn.heres_your_socket(wrapped_child)
76
+ wrapped_child.call_back_to_this(conn) # calls post_init for us
77
+ yield conn if block_given?
78
+ end
79
+
80
+ # Start a TCP server on the given address and port
81
+ def start_server(addr, port, handler = Connection, *args, &block)
82
+ # make sure we're a 'real' class here
83
+ klass = if (handler and handler.is_a?(Class))
84
+ handler
85
+ else
86
+ Class.new( Connection ) {handler and include handler}
87
+ end
88
+
89
+ server = Coolio::TCPServer.new(addr, port, CallsBackToEM, *args) do |wrapped_child|
90
+ conn = klass.new(wrapped_child)
91
+ conn.heres_your_socket(wrapped_child) # ideally NOT have this :)
92
+ wrapped_child.call_back_to_this(conn)
93
+ block.call(conn) if block
94
+ end
95
+
96
+ server.attach(Coolio::Loop.default)
97
+ end
98
+
99
+ def stop_server(server)
100
+ server.close
101
+ end
102
+
103
+ # Set the maximum number of descriptors available to this process
104
+ def set_descriptor_table_size(nfds)
105
+ Coolio::Utils.maxfds = nfds
106
+ end
107
+
108
+ # Compatibility noop. Handled automatically by libev
109
+ def epoll; end
110
+
111
+ # Compatibility noop. Handled automatically by libev
112
+ def kqueue; end
113
+ end
114
+
115
+ class CallsBackToEM < Coolio::TCPSocket
116
+ class ConnectTimer < Coolio::TimerWatcher
117
+ attr_accessor :parent
118
+ def on_timer
119
+ @parent.connection_has_timed_out
120
+ end
121
+ end
122
+
123
+ def call_back_to_this parent
124
+ @call_back_to_this = parent
125
+ parent.post_init
126
+ end
127
+
128
+ def on_connect
129
+ # @connection_timer.detach if @connection_timer
130
+ # won't need that anymore :) -- with server connecteds we don't have it, anyway
131
+
132
+ # TODO should server accepted's call this? They don't currently
133
+ # [and can't, since on_connect gets called basically in the initializer--needs some code love for that to happen :)
134
+ @call_back_to_this.connection_completed if @call_back_to_this
135
+ end
136
+
137
+ def connection_has_timed_out
138
+ return if closed?
139
+
140
+ # wonder if this works when you're within a half-connected phase.
141
+ # I think it does. What about TCP state?
142
+ close unless closed?
143
+ @call_back_to_this.unbind
144
+ end
145
+
146
+ def on_write_complete
147
+ close if @should_close_after_writing
148
+ end
149
+
150
+ def should_close_after_writing
151
+ @should_close_after_writing = true;
152
+ end
153
+
154
+ def on_close
155
+ @call_back_to_this.unbind # about the same ltodo check if they ARE the same here
156
+ end
157
+
158
+ def on_resolve_failed
159
+ fail
160
+ end
161
+
162
+ def on_connect_failed
163
+ fail
164
+ end
165
+
166
+ def on_read(data)
167
+ @call_back_to_this.receive_data data
168
+ end
169
+
170
+ def fail
171
+ #@connection_timer.detch if @connection_timer
172
+ @call_back_to_this.unbind
173
+ end
174
+
175
+ def self.connect(*args)
176
+ a = super *args
177
+ # the connect timer currently kills TCPServer classes. I'm not sure why.
178
+ #@connection_timer = ConnectTimer.new(14) # needs to be at least higher than 12 :)
179
+ #@connection_timer.parent = a
180
+ #@connection_timer.attach(Coolio::Loop.default)
181
+ a
182
+ end
183
+ end
184
+
185
+ class Connection
186
+ def self.new(*args)
187
+ allocate#.instance_eval do
188
+ # initialize *args
189
+ #end
190
+ end
191
+
192
+ # we will need to call 'their functions' appropriately -- the commented out ones, here
193
+ #
194
+ # Callback fired when connection is created
195
+ def post_init
196
+ # I thought we were 'overriding' EM's existing methods, here.
197
+ # Huh? Why do we have to define these then?
198
+ end
199
+
200
+ # Callback fired when connection is closed
201
+ def unbind; end
202
+
203
+ # Callback fired when data is received
204
+ # def receive_data(data); end
205
+ def heres_your_socket(instantiated_coolio_socket)
206
+ instantiated_coolio_socket.call_back_to_this self
207
+ @wrapped_coolio = instantiated_coolio_socket
208
+ end
209
+
210
+ # Send data to the current connection -- called by them
211
+ def send_data(data)
212
+ @wrapped_coolio.write data
213
+ end
214
+
215
+ # Close the connection, optionally after writing
216
+ def close_connection(after_writing = false)
217
+ return close_connection_after_writing if after_writing
218
+ @wrapped_coolio.close
219
+ end
220
+
221
+ # Close the connection after all data has been written
222
+ def close_connection_after_writing
223
+ @wrapped_coolio.output_buffer_size.zero? ? @wrapped_coolio.close : @wrapped_coolio.should_close_after_writing
224
+ end
225
+
226
+ def get_peername
227
+ family, port, host_name, host_ip = @wrapped_coolio.peeraddr
228
+ Socket.pack_sockaddr_in(port, host_ip) # pack it up :)
229
+ end
230
+ end
231
+ end
232
+
233
+ # Shortcut constant
234
+ EM = EventMachine
@@ -0,0 +1,427 @@
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 "cool.io/custom_require"
9
+ cool_require 'http11_client'
10
+
11
+ module Coolio
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.inject({}) { |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, path, query)
83
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
84
+ end
85
+
86
+ def encode_query(path, query)
87
+ return path unless query
88
+ path + "?" + 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.inject('') do |result, (key, value)|
103
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
104
+ key = key.split('-').map { |k| k.capitalize }.join('-')
105
+ result << encode_field(key, value)
106
+ end
107
+ end
108
+
109
+ def encode_cookies(cookies)
110
+ cookies.inject('') { |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 Coolio::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 = Coolio::Loop.default
122
+ # client = Coolio::HttpClient.connect("www.google.com").attach(loop)
123
+ # client.request('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 = ::IO::Buffer.new
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, path, options = {})
171
+ raise ArgumentError, "invalid request path" unless /^\// === path
172
+ raise RuntimeError, "request already sent" if @requested
173
+
174
+ @method, @path, @options = method, path, options
175
+ @requested = true
176
+
177
+ return unless @connected
178
+ send_request
179
+ end
180
+
181
+ # Enable the HttpClient if it has been disabled
182
+ def enable
183
+ super
184
+ dispatch unless @data.empty?
185
+ end
186
+
187
+ # Called when response header has been received
188
+ def on_response_header(response_header)
189
+ end
190
+
191
+ # Called when part of the body has been read
192
+ def on_body_data(data)
193
+ STDOUT.write data
194
+ STDOUT.flush
195
+ end
196
+
197
+ # Called when the request has completed
198
+ def on_request_complete
199
+ @state == :finished ? close : @state = :finished
200
+ end
201
+
202
+ # called by close
203
+ def on_close
204
+ if @state != :finished and @state == :body
205
+ on_request_complete
206
+ end
207
+ end
208
+
209
+ # Called when an error occurs dispatching the request
210
+ def on_error(reason)
211
+ close
212
+ raise RuntimeError, reason
213
+ end
214
+
215
+ #########
216
+ protected
217
+ #########
218
+
219
+ #
220
+ # Coolio callbacks
221
+ #
222
+
223
+ def on_connect
224
+ @connected = true
225
+ send_request if @method and @path
226
+ end
227
+
228
+ def on_read(data)
229
+ @data << data
230
+ dispatch
231
+ end
232
+
233
+ #
234
+ # Request sending
235
+ #
236
+
237
+ def send_request
238
+ send_request_header
239
+ send_request_body
240
+ end
241
+
242
+ def send_request_header
243
+ query = @options[:query]
244
+ head = @options[:head] ? munge_header_keys(@options[:head]) : {}
245
+ cookies = @options[:cookies]
246
+ body = @options[:body]
247
+
248
+ # Set the Host header if it hasn't been specified already
249
+ head['host'] ||= encode_host
250
+
251
+ # Set the Content-Length if it hasn't been specified already and a body was given
252
+ head['content-length'] ||= body ? body.length : 0
253
+
254
+ # Set the User-Agent if it hasn't been specified
255
+ head['user-agent'] ||= "Coolio #{Coolio::VERSION}"
256
+
257
+ # Default to Connection: close
258
+ head['connection'] ||= 'close'
259
+
260
+ # Build the request
261
+ request_header = encode_request(@method, @path, query)
262
+ request_header << encode_headers(head)
263
+ request_header << encode_cookies(cookies) if cookies
264
+ request_header << CRLF
265
+
266
+ write request_header
267
+ end
268
+
269
+ def send_request_body
270
+ write @options[:body] if @options[:body]
271
+ end
272
+
273
+ #
274
+ # Response processing
275
+ #
276
+
277
+ def dispatch
278
+ while enabled? and case @state
279
+ when :response_header
280
+ parse_response_header
281
+ when :chunk_header
282
+ parse_chunk_header
283
+ when :chunk_body
284
+ process_chunk_body
285
+ when :chunk_footer
286
+ process_chunk_footer
287
+ when :response_footer
288
+ process_response_footer
289
+ when :body
290
+ process_body
291
+ when :finished, :invalid
292
+ break
293
+ else raise RuntimeError, "invalid state: #{@state}"
294
+ end
295
+ end
296
+ end
297
+
298
+ def parse_header(header)
299
+ return false if @data.empty?
300
+
301
+ begin
302
+ @parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
303
+ rescue Coolio::HttpClientParserError
304
+ on_error "invalid HTTP format, parsing fails"
305
+ @state = :invalid
306
+ end
307
+
308
+ return false unless @parser.finished?
309
+
310
+ # Clear parsed data from the buffer
311
+ @data.read(@parser_nbytes)
312
+ @parser.reset
313
+ @parser_nbytes = 0
314
+
315
+ true
316
+ end
317
+
318
+ def parse_response_header
319
+ return false unless parse_header(@response_header)
320
+
321
+ unless @response_header.http_status and @response_header.http_reason
322
+ on_error "no HTTP response"
323
+ @state = :invalid
324
+ return false
325
+ end
326
+
327
+ on_response_header(@response_header)
328
+
329
+ if @response_header.chunked_encoding?
330
+ @state = :chunk_header
331
+ else
332
+ @state = :body
333
+ @bytes_remaining = @response_header.content_length
334
+ end
335
+
336
+ true
337
+ end
338
+
339
+ def parse_chunk_header
340
+ return false unless parse_header(@chunk_header)
341
+
342
+ @bytes_remaining = @chunk_header.chunk_size
343
+ @chunk_header = HttpChunkHeader.new
344
+
345
+ @state = @bytes_remaining > 0 ? :chunk_body : :response_footer
346
+ true
347
+ end
348
+
349
+ def process_chunk_body
350
+ if @data.size < @bytes_remaining
351
+ @bytes_remaining -= @data.size
352
+ on_body_data @data.read
353
+ return false
354
+ end
355
+
356
+ on_body_data @data.read(@bytes_remaining)
357
+ @bytes_remaining = 0
358
+
359
+ @state = :chunk_footer
360
+ true
361
+ end
362
+
363
+ def process_chunk_footer
364
+ return false if @data.size < 2
365
+
366
+ if @data.read(2) == CRLF
367
+ @state = :chunk_header
368
+ else
369
+ on_error "non-CRLF chunk footer"
370
+ @state = :invalid
371
+ end
372
+
373
+ true
374
+ end
375
+
376
+ def process_response_footer
377
+ return false if @data.size < 2
378
+
379
+ if @data.read(2) == CRLF
380
+ if @data.empty?
381
+ on_request_complete
382
+ @state = :finished
383
+ else
384
+ on_error "garbage at end of chunked response"
385
+ @state = :invalid
386
+ end
387
+ else
388
+ on_error "non-CRLF response footer"
389
+ @state = :invalid
390
+ end
391
+
392
+ false
393
+ end
394
+
395
+ def process_body
396
+ if @bytes_remaining.nil?
397
+ on_body_data @data.read
398
+ return false
399
+ end
400
+
401
+ if @bytes_remaining.zero?
402
+ on_request_complete
403
+ @state = :finished
404
+ return false
405
+ end
406
+
407
+ if @data.size < @bytes_remaining
408
+ @bytes_remaining -= @data.size
409
+ on_body_data @data.read
410
+ return false
411
+ end
412
+
413
+ on_body_data @data.read(@bytes_remaining)
414
+ @bytes_remaining = 0
415
+
416
+ if @data.empty?
417
+ on_request_complete
418
+ @state = :finished
419
+ else
420
+ on_error "garbage at end of body"
421
+ @state = :invalid
422
+ end
423
+
424
+ false
425
+ end
426
+ end
427
+ end