about summary refs log tree commit homepage
path: root/lib/rainbows/response.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rainbows/response.rb')
-rw-r--r--lib/rainbows/response.rb197
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