cool.io 1.2.0-x86-mswin32-60

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.
Files changed (79) hide show
  1. data/.gitignore +26 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +5 -0
  4. data/CHANGES.md +177 -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