diff options
Diffstat (limited to 'lib/yahns/http_response.rb')
-rw-r--r-- | lib/yahns/http_response.rb | 183 |
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 |