kcar.git  about / heads / tags
bytestream to Rack response converter
blob 58a7148b3fd220689bf944161ff32474bcccf0fb 5028 bytes (raw)
$ git show HEAD:lib/kcar/response.rb	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
 
# -*- encoding: binary -*-


# This may be used to generate a Rack response synchronously.

class Kcar::Response
  # :stopdoc:
  attr_accessor :sock, :hdr, :unchunk, :buf, :parser
  # :startdoc:

  # By default we readpartial at most 16K off a socket at once
  READ_SIZE = 0x4000

  # initializes a socket, +sock+ must respond to the "readpartial"
  # method.  +unchunk+ may be set to disable transparent unchunking
  # +hdr+ may be a Hash, Array, or Rack::Utils::HeaderHash
  def initialize(sock, hdr = {}, unchunk = true)
    @sock, @hdr, @unchunk, @buf =  sock, hdr, unchunk, ""
    @parser = Kcar::Parser.new
  end

  # returns a 3-element array that resembles a Rack response, but is
  # more useful for additional processing by other code.
  #
  #    [ status, headers, body ]
  #
  # Use Kcar::Response#rack if you want to guarantee a proper Rack response.
  #
  # this method will not return until the response headers are fully parsed,
  # but the body returned will be this Kcar::Response handler itself.
  # +unchunk+ must be true to guarantee trailers will be stored in the
  # returned +header+ object
  def read
    @buf << @sock.readpartial(READ_SIZE) if @buf.empty?
    until response = @parser.headers(@hdr, @buf)
      @buf << @sock.readpartial(READ_SIZE)
    end
    response << self
  end

  # returns a 3-element array suitable for use as a Rack response:
  #    [ status, headers, body ]
  #
  # this method will not return until the response headers are fully parsed,
  # but the body returned will be this Kcar::Response handler itself.
  # It is not guaranteed that trailers will be stored in the returned +header+
  def rack
    @unchunk = false
    read
  end

  # this is expected to be called by our Rack server, it will close
  # our given +sock+ object if keepalive is not used otherwise it
  # will just reset the parser and clear the header object
  def close
    @parser.keepalive? ? reset : @sock.close
  end

  # this method allows our Kcar::Response object to be used as a Rack response
  # body.  It may only be called once (usually by a Rack server) as it streams
  # the response body off the our socket object.
  def each
    return if @parser.body_eof?
    if @unchunk
      @parser.chunked? ? each_unchunk { |buf| yield buf } :
                         each_identity { |buf| yield buf }
    else
      @parser.chunked? ? each_rechunk { |buf| yield buf } :
                         each_identity { |buf| yield buf }
    end
  end

  # :stopdoc:
  def reset
    @parser.reset
    @hdr.clear
  end

  def each_rechunk
    # 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 << "\r\n".freeze)
      end
      break if @parser.body_eof?
    end while @buf << @sock.readpartial(READ_SIZE, dst)

    yield "0\r\n".freeze

    until @parser.trailers(@hdr, @buf)
      @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.
    trailers = @parser.extract_trailers(@hdr)
    yield(trailers.map! { |k,v| "#{k}: #{v}\r\n" }.join << "\r\n".freeze)
  end

  def each_until_eof
    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)
  rescue EOFError
  end

  def each_identity
    len = @parser.body_bytes_left
    if len == nil
      each_until_eof { |x| yield x }
    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

      if len > 0
        begin
          len -= @sock.readpartial(len > READ_SIZE ? READ_SIZE : len, dst).size
          yield dst
        end while len > 0
        dst.clear
      end
    end
    @parser.body_bytes_left = 0
  end

  def each_unchunk
    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...
    until @parser.headers(@hdr, @buf)
      @buf << @sock.readpartial(READ_SIZE, dst)
    end
  end
  # :startdoc:
end

git clone https://yhbt.net/kcar.git