about summary refs log tree commit homepage
path: root/lib/yahns/http_response.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/yahns/http_response.rb')
-rw-r--r--lib/yahns/http_response.rb183
1 files changed, 183 insertions, 0 deletions
diff --git a/lib/yahns/http_response.rb b/lib/yahns/http_response.rb
new file mode 100644
index 0000000..aad2762
--- /dev/null
+++ b/lib/yahns/http_response.rb
@@ -0,0 +1,183 @@
+# -*- encoding: binary -*-
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'stream_file'
+
+# Writes a Rack response to your client using the HTTP/1.1 specification.
+# You use it by simply doing:
+#
+#   status, headers, body = rack_app.call(env)
+#   http_response_write(status, headers, body)
+#
+# Most header correctness (including Content-Length and Content-Type)
+# is the job of Rack, with the exception of the "Date" header.
+module Yahns::HttpResponse # :nodoc:
+  include Unicorn::HttpResponse
+
+  # avoid GC overhead for frequently used-strings:
+  CONN_KA = "Connection: keep-alive\r\n\r\n"
+  CONN_CLOSE = "Connection: close\r\n\r\n"
+  Z = ""
+  RESPONSE_START = "HTTP/1.1 "
+
+  def response_start
+    @response_start_sent ? Z : RESPONSE_START
+  end
+
+  def response_wait_write(rv)
+    # call the kgio_wait_readable or kgio_wait_writable method
+    ok = __send__("kgio_#{rv}") and return ok
+    k = self.class
+    k.logger.info("fd=#{fileno} ip=#@kgio_addr timeout on :#{rv} after "\
+                  "#{k.client_timeout}s")
+    nil
+  end
+
+  def err_response(code)
+    "#{response_start}#{CODES[code]}\r\n\r\n"
+  end
+
+  def response_header_blocked(ret, header, body, alive, offset, count)
+    if body.respond_to?(:to_path)
+      alive = Yahns::StreamFile.new(body, alive, offset, count)
+      body = nil
+    end
+    wbuf = Yahns::Wbuf.new(body, alive)
+    rv = wbuf.wbuf_write(self, header)
+    body.each { |chunk| rv = wbuf.wbuf_write(self, chunk) } if body
+    wbuf_maybe(wbuf, rv, alive)
+  end
+
+  def wbuf_maybe(wbuf, rv, alive)
+    case rv # trysendfile return value
+    when nil
+      case alive
+      when :delete
+        @state = :delete
+      when true, false
+        http_response_done(alive)
+      end
+    else
+      @state = wbuf
+      rv
+    end
+  end
+
+  def http_response_done(alive)
+    @input = @input.discard if @input
+    if alive
+      @response_start_sent = false
+      # @hs.buf will have data if the client pipelined
+      if @hs.buf.empty?
+        @state = :headers
+        :wait_readable
+      else
+        @state = :pipelined
+        # may need to wait for readability if SSL,
+        # only need writability if plain TCP
+        :wait_readwrite
+      end
+    else
+      # shutdown is needed in case the app forked, we rescue here since
+      # StreamInput may issue shutdown as well
+      shutdown rescue nil
+      nil # trigger close
+    end
+  end
+
+  # writes the rack_response to socket as an HTTP response
+  # returns :wait_readable, :wait_writable, :forget, or nil
+  def http_response_write(status, headers, body)
+    status = CODES[status.to_i] || status
+    offset = 0
+    count = hijack = nil
+    alive = @hs.next?
+
+    if @hs.headers?
+      buf = "#{response_start}#{status}\r\nDate: #{httpdate}\r\n"
+      headers.each do |key, value|
+        case key
+        when %r{\ADate\z}
+          next
+        when %r{\AContent-Range\z}i
+          if %r{\Abytes (\d+)-(\d+)/\d+\z} =~ value
+            offset = $1.to_i
+            count = $2.to_i - offset + 1
+          end
+        when %r{\AConnection\z}i
+          # allow Rack apps to tell us they want to drop the client
+          alive = !!(value =~ /\bclose\b/i)
+        when "rack.hijack"
+          hijack = value
+          body = nil # ensure we do not close body
+        else
+          if value =~ /\n/
+            # avoiding blank, key-only cookies with /\n+/
+            buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
+          else
+            buf << "#{key}: #{value}\r\n"
+          end
+        end
+      end
+      buf << (alive ? CONN_KA : CONN_CLOSE)
+      case rv = kgio_trywrite(buf)
+      when nil # all done, likely
+        break
+      when String
+        buf = rv # hope the skb grows
+      when :wait_writable, :wait_readable
+        if self.class.output_buffering
+          alive = hijack ? hijack : alive
+          rv = response_header_blocked(rv, buf, body, alive, offset, count)
+          body = nil # ensure we do not close body in ensure
+          return rv
+        else
+          response_wait_write(rv) or return
+        end
+      end while true
+    end
+
+    if hijack
+      hijack.call(self)
+      return :delete # trigger EPOLL_CTL_DEL
+    end
+
+    if body.respond_to?(:to_path)
+      @state = body = Yahns::StreamFile.new(body, alive, offset, count)
+      return step_write
+    end
+
+    wbuf = rv = nil
+    body.each do |chunk|
+      if wbuf
+        rv = wbuf.wbuf_write(self, chunk)
+      else
+        case rv = kgio_trywrite(chunk)
+        when nil # all done, likely and good!
+          break
+        when String
+          chunk = rv # hope the skb grows when we loop into the trywrite
+        when :wait_writable, :wait_readable
+          if self.class.output_buffering
+            wbuf = Yahns::Wbuf.new(body, alive)
+            rv = wbuf.wbuf_write(self, chunk)
+            break
+          else
+            response_wait_write(rv) or return
+          end
+        end while true
+      end
+    end
+
+    # if we buffered the write body, we must return :wait_writable
+    # (or :wait_readable for SSL) and hit Yahns::HttpClient#step_write
+    if wbuf
+      body = nil # ensure we do not close the body in ensure
+      wbuf_maybe(wbuf, rv, alive)
+    else
+      http_response_done(alive)
+    end
+  ensure
+    body.respond_to?(:close) and body.close
+  end
+end