rev + em: more easily allow Content-Length in pipe responses
[rainbows.git] / lib / rainbows / ev_core.rb
blobdbcdebae9cc35f0662e3b8d22a77d69a9ef15586
1 # -*- encoding: binary -*-
2 # :enddoc:
3 module Rainbows
5   # base module for evented models like Rev and EventMachine
6   module EvCore
7     include Unicorn
8     include Rainbows::Const
9     include Rainbows::Response
10     G = Rainbows::G
11     NULL_IO = Unicorn::HttpRequest::NULL_IO
13     # Apps may return this Rack response: AsyncResponse = [ -1, {}, [] ]
14     ASYNC_CALLBACK = "async.callback".freeze
16     ASYNC_CLOSE = "async.close".freeze
18     def post_init
19       @remote_addr = Rainbows.addr(@_io)
20       @env = {}
21       @hp = HttpParser.new
22       @state = :headers # [ :body [ :trailers ] ] :app_call :close
23       @buf = ""
24     end
26     # graceful exit, like SIGQUIT
27     def quit
28       @state = :close
29     end
31     def handle_error(e)
32       msg = Error.response(e) and write(msg)
33       ensure
34         quit
35     end
37     # returns whether to enable response chunking for autochunk models
38     def stream_response_headers(status, headers)
39       if headers['Content-Length']
40         rv = false
41       else
42         rv = !!(headers['Transfer-Encoding'] =~ %r{\Achunked\z}i)
43         rv = false if headers.delete('X-Rainbows-Autochunk') == 'no'
44       end
45       headers[CONNECTION] = CLOSE # TODO: allow keep-alive
46       write(response_header(status, headers))
47       rv
48     end
50     # TeeInput doesn't map too well to this right now...
51     def on_read(data)
52       case @state
53       when :headers
54         @hp.headers(@env, @buf << data) or return
55         @state = :body
56         len = @hp.content_length
57         if len == 0
58           @input = NULL_IO
59           app_call # common case
60         else # nil or len > 0
61           # since we don't do streaming input, we have no choice but
62           # to take over 100-continue handling from the Rack application
63           if @env[HTTP_EXPECT] =~ /\A100-continue\z/i
64             write(EXPECT_100_RESPONSE)
65             @env.delete(HTTP_EXPECT)
66           end
67           @input = CapInput.new(len, self)
68           @hp.filter_body(@buf2 = "", @buf)
69           @input << @buf2
70           on_read("")
71         end
72       when :body
73         if @hp.body_eof?
74           @state = :trailers
75           on_read(data)
76         elsif data.size > 0
77           @hp.filter_body(@buf2, @buf << data)
78           @input << @buf2
79           on_read("")
80         end
81       when :trailers
82         if @hp.trailers(@env, @buf << data)
83           @input.rewind
84           app_call
85         end
86       end
87       rescue => e
88         handle_error(e)
89     end
91     class CapInput < Struct.new(:io, :client, :bytes_left)
92       MAX_BODY = Unicorn::Const::MAX_BODY
93       Util = Unicorn::Util
95       def self.err(client, msg)
96         client.write(Const::ERROR_413_RESPONSE)
97         client.quit
99         # zip back up the stack
100         raise IOError, msg, []
101       end
103       def self.new(len, client)
104         max = Rainbows.max_bytes
105         if len
106           if max && (len > max)
107             err(client, "Content-Length too big: #{len} > #{max}")
108           end
109           len <= MAX_BODY ? StringIO.new("") : Util.tmpio
110         else
111           max ? super(Util.tmpio, client, max) : Util.tmpio
112         end
113       end
115       def <<(buf)
116         if (self.bytes_left -= buf.size) < 0
117           io.close
118           CapInput.err(client, "chunked request body too big")
119         end
120         io << buf
121       end
123       def gets; io.gets; end
124       def each(&block); io.each(&block); end
125       def size; io.size; end
126       def rewind; io.rewind; end
127       def read(*args); io.read(*args); end
129     end
131   end