diff options
Diffstat (limited to 'lib/rainbows/response.rb')
-rw-r--r-- | lib/rainbows/response.rb | 197 |
1 files changed, 158 insertions, 39 deletions
diff --git a/lib/rainbows/response.rb b/lib/rainbows/response.rb index ca381b8..c0d0740 100644 --- a/lib/rainbows/response.rb +++ b/lib/rainbows/response.rb @@ -3,60 +3,179 @@ require 'time' # for Time#httpdate module Rainbows::Response - autoload :Body, 'rainbows/response/body' - autoload :Range, 'rainbows/response/range' - + CRLF = Unicorn::HttpResponse::CRLF CODES = Unicorn::HttpResponse::CODES - CRLF = "\r\n" + Close = "close" + KeepAlive = "keep-alive" + + # private file class for IO objects opened by Rainbows! itself (and not + # the app or middleware) + class F < File; end - # freeze headers we may set as hash keys for a small speedup - CONNECTION = "Connection".freeze - CLOSE = "close" - KEEP_ALIVE = "keep-alive" - HH = Rack::Utils::HeaderHash + # called after forking + def self.setup(klass) + Kgio.accept_class = Rainbows::Client + 0 == Rainbows::G.kato and Rainbows::HttpParser.keepalive_requests = 0 + end - def response_header(status, headers) + def write_headers(status, headers, alive) + @hp.headers? or return status = CODES[status.to_i] || status - rv = "HTTP/1.1 #{status}\r\n" \ - "Date: #{Time.now.httpdate}\r\n" \ - "Status: #{status}\r\n" + buf = "HTTP/1.1 #{status}\r\n" \ + "Date: #{Time.now.httpdate}\r\n" \ + "Status: #{status}\r\n" \ + "Connection: #{alive ? KeepAlive : Close}\r\n" headers.each do |key, value| - next if %r{\A(?:X-Rainbows-|Date\z|Status\z)}i =~ key + next if %r{\A(?:X-Rainbows-|Date\z|Status\z\|Connection\z)}i =~ key if value =~ /\n/ # avoiding blank, key-only cookies with /\n+/ - rv << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join('') + buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join else - rv << "#{key}: #{value}\r\n" + buf << "#{key}: #{value}\r\n" end end - rv << CRLF + write(buf << CRLF) end - # called after forking - def self.setup(klass) - if 0 == Rainbows::G.kato - KEEP_ALIVE.replace(CLOSE) - Rainbows::HttpParser.keepalive_requests = 0 - end - range_class = body_class = klass - case Rainbows::Const::RACK_DEFAULTS['rainbows.model'] - when :WriterThreadSpawn - body_class = Rainbows::WriterThreadSpawn::Client - range_class = Rainbows::HttpServer - when :EventMachine, :NeverBlock - range_class = nil # :< - end - return if body_class.included_modules.include?(Body) - body_class.__send__(:include, Body) - sf = IO.respond_to?(:copy_stream) || IO.method_defined?(:sendfile_nonblock) - if range_class - range_class.__send__(:include, sf ? Range : NoRange) + def close_if_private(io) + io.close if F === io + end + + def io_for_fd(fd) + Rainbows::FD_MAP.delete(fd) || F.for_fd(fd) + end + + # to_io is not part of the Rack spec, but make an exception here + # since we can conserve path lookups and file descriptors. + # \Rainbows! will never get here without checking for the existence + # of body.to_path first. + def body_to_io(body) + if body.respond_to?(:to_io) + body.to_io + else + # try to take advantage of Rainbows::DevFdResponse, calling F.open + # is a last resort + path = body.to_path + %r{\A/dev/fd/(\d+)\z} =~ path ? io_for_fd($1.to_i) : F.open(path) end end - module NoRange - # dummy method if we can't send range responses - def make_range!(env, status, headers) + module Each + # generic body writer, used for most dynamically-generated responses + def write_body_each(body) + body.each { |chunk| write(chunk) } + end + + # generic response writer, used for most dynamically-generated responses + # and also when IO.copy_stream and/or IO#sendfile_nonblock is unavailable + def write_response(status, headers, body, alive) + write_headers(status, headers, alive) + write_body_each(body) + ensure + body.close if body.respond_to?(:close) end end + include Each + + if IO.method_defined?(:sendfile_nonblock) + module Sendfile + def write_body_file(body, range) + io = body_to_io(body) + range ? sendfile(io, range[0], range[1]) : sendfile(io, 0) + ensure + close_if_private(io) + end + end + include Sendfile + end + + if IO.respond_to?(:copy_stream) + unless IO.method_defined?(:sendfile_nonblock) + module CopyStream + def write_body_file(body, range) + range ? IO.copy_stream(body, self, range[1], range[0]) : + IO.copy_stream(body, self, nil, 0) + end + end + include CopyStream + end + + # write_body_stream is an alias for write_body_each if IO.copy_stream + # isn't used or available. + def write_body_stream(body) + IO.copy_stream(io = body_to_io(body), self) + ensure + close_if_private(io) + end + else # ! IO.respond_to?(:copy_stream) + alias write_body_stream write_body_each + end # ! IO.respond_to?(:copy_stream) + + if IO.method_defined?(:sendfile_nonblock) || IO.respond_to?(:copy_stream) + HTTP_RANGE = 'HTTP_RANGE' + Content_Range = 'Content-Range'.freeze + Content_Length = 'Content-Length'.freeze + + # This does not support multipart responses (does anybody actually + # use those?) + def sendfile_range(status, headers) + 200 == status.to_i && + /\Abytes=(\d+-\d*|\d*-\d+)\z/ =~ @hp.env[HTTP_RANGE] or + return + a, b = $1.split(/-/) + + headers = Rack::Utils::HeaderHash.new(headers) + clen = headers[Content_Length] or return + size = clen.to_i + + if b.nil? # bytes=M- + offset = a.to_i + count = size - offset + elsif a.empty? # bytes=-N + offset = size - b.to_i + count = size - offset + else # bytes=M-N + offset = a.to_i + count = b.to_i + 1 - offset + end + + if 0 > count || offset >= size + return 416, headers, nil + else + count = size if count > size + headers[Content_Length] = count.to_s + headers[Content_Range] = "bytes #{offset}-#{offset+count-1}/#{clen}" + return 206, headers, [ offset, count ] + end + end + + def write_response_path(status, headers, body, alive) + if File.file?(body.to_path) + if r = sendfile_range(status, headers) + status, headers, range = r + write_headers(status, headers, alive) + write_body_file(body, range) if range + else + write_headers(status, headers, alive) + write_body_file(body, nil) + end + else + write_headers(status, headers, alive) + write_body_stream(body) + end + ensure + body.close if body.respond_to?(:close) + end + + module ToPath + def write_response(status, headers, body, alive) + if body.respond_to?(:to_path) + write_response_path(status, headers, body, alive) + else + super + end + end + end + include ToPath + end # IO.respond_to?(:copy_stream) || IO.method_defined?(:sendfile_nonblock) end |