about summary refs log tree commit homepage
path: root/lib/kcar/response.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/kcar/response.rb')
-rw-r--r--lib/kcar/response.rb130
1 files changed, 130 insertions, 0 deletions
diff --git a/lib/kcar/response.rb b/lib/kcar/response.rb
new file mode 100644
index 0000000..724e1df
--- /dev/null
+++ b/lib/kcar/response.rb
@@ -0,0 +1,130 @@
+# -*- encoding: binary -*-
+
+module Kcar
+class Response < Struct.new(:sock, :unchunk, :hdr, :buf, :parser)
+
+  LAST_CHUNK = "0\r\n"
+  CRLF = "\r\n"
+  READ_SIZE = 0x4000
+
+  def initialize(sock, unchunk = true, hdr = {})
+    super(sock, unchunk, hdr, "", Parser.new)
+  end
+
+  def read
+    buf << sock.readpartial(READ_SIZE) if buf.empty?
+    while (response = parser.headers(hdr, buf)).nil?
+      buf << sock.readpartial(READ_SIZE)
+    end
+    response << self
+  end
+
+  def reset
+    parser.reset
+    hdr.clear
+  end
+
+  # this method allows Kcar::Session to be used as a Rack response body
+  def each(&block)
+    return if parser.body_eof?
+    if unchunk
+      parser.chunked? ? each_unchunk(&block) : each_identity(&block)
+    else
+      if parser.keepalive?
+        parser.chunked? ? each_rechunk(&block) : each_identity(&block)
+      else
+        each_until_eof(&block) # fastest path
+      end
+    end
+    rescue EOFError
+  end
+
+  def each_rechunk(&block)
+    # We have to filter_body to keep track of parser state
+    # (which sucks).  Also, as a benefit to clients we'll rechunk
+    # to increase the likelyhood of network transfers being on
+    # chunk boundaries so we're less likely to trigger bugs in
+    # other people's code :)
+    dst = ""
+    begin
+      parser.filter_body(dst, buf) and break
+      size = dst.size
+      if size > 0
+        yield("#{size.to_s(16)}\r\n")
+        yield(dst << CRLF)
+      end
+      break if parser.body_eof?
+    end while buf << sock.readpartial(READ_SIZE, dst)
+
+    yield LAST_CHUNK
+
+    while parser.trailers(hdr, buf).nil?
+      buf << sock.readpartial(READ_SIZE, dst)
+    end
+
+    # since Rack does not provide a way to explicitly send trailers
+    # in the response, we'll just yield a stringified version to our
+    # server and pretend it's part of the body.
+    if trailers = parser.extract_trailers(hdr)
+      yield(trailers.map! { |k,v| "#{k}: #{v}\r\n" }.join("") << "\r\n")
+    end
+  end
+
+  # this is called by our Rack server
+  def close
+    parser.keepalive? ? reset : sock.close
+  end
+
+  def each_until_eof(&block)
+    yield buf unless buf.empty?
+    # easy, just read and write everything until EOFError
+    dst = sock.readpartial(READ_SIZE)
+    begin
+      yield dst
+    end while sock.readpartial(READ_SIZE, dst)
+  end
+
+  def each_identity(&block)
+    len = parser.body_bytes_left
+    if len.nil?
+      each_until_eof(&block)
+    else
+      dst = buf
+      if dst.size > 0
+        # in case of keepalive we need to read the second response,
+        # so modify buf so that the second response is at the front
+        # of the buffer
+        if dst.size >= len
+          tmp = dst[len, dst.size]
+          dst = dst[0, len]
+          buf.replace(tmp)
+        end
+
+        len -= dst.size
+        yield dst
+      end
+      while len > 0
+        len -= sock.readpartial(len > READ_SIZE ? CHUNK_SIZE : len, dst).size
+        yield dst
+      end
+    end
+  end
+
+  def each_unchunk(&block)
+    dst = ""
+    begin
+      parser.filter_body(dst, buf) and break
+      yield dst if dst.size > 0
+      parser.body_eof? and break
+    end while buf << sock.readpartial(READ_SIZE, dst)
+
+    # we can't pass trailers to the client since we unchunk
+    # the response, so just read them off the socket and
+    # stash them in hdr just in case...
+    while parser.headers(hdr, buf).nil?
+      buf << sock.readpartial(READ_SIZE, dst)
+    end
+  end
+
+end # class Session
+end # module Kcar