File: protocol.rb

package info (click to toggle)
ruby-ftw 0.0.49-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 548 kB
  • sloc: ruby: 1,922; makefile: 5
file content (203 lines) | stat: -rw-r--r-- 6,723 bytes parent folder | download | duplicates (4)
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
require "ftw/namespace"
require "ftw/crlf"
require "cabin"
require "logger"

# This module provides web protocol handling as a mixin.
module FTW::Protocol
  include FTW::CRLF

  # Read an HTTP message from a given connection
  #
  # This method blocks until a full http message header has been consumed
  # (request *or* response)
  #
  # The body of the message, if any, will not be consumed, and the read
  # position for the connection will be left at the end of the message headers.
  # 
  # The 'connection' object must respond to #read(timeout) and #pushback(string)
  def read_http_message(connection)
    parser = HTTP::Parser.new
    headers_done = false
    parser.on_headers_complete = proc { headers_done = true; :stop }

    # headers_done will be set to true when parser finishes parsing the http
    # headers for this request
    while !headers_done
      # TODO(sissel): This read could toss an exception of the server aborts
      # prior to sending the full headers. Figure out a way to make this happy.
      # Perhaps fabricating a 500 response?
      data = connection.read(16384)

      # Feed the data into the parser. Offset will be nonzero if there's 
      # extra data beyond the header.
      offset = parser << data
    end

    # If we consumed part of the body while parsing headers, put it back
    # onto the connection's read buffer so the next consumer can use it.
    if offset < data.length
      connection.pushback(data[offset .. -1])
    end

    # This will have an 'http_method' if it's a request
    if !parser.http_method.nil?
      # have http_method, so this is an HTTP Request message
      request = FTW::Request.new
      request.method = parser.http_method
      request.request_uri = parser.request_url
      request.version = "#{parser.http_major}.#{parser.http_minor}".to_f
      parser.headers.each { |field, value| request.headers.add(field, value) }
      return request
    else
      # otherwise, no http_method, so this is an HTTP Response message
      response = FTW::Response.new
      response.version = "#{parser.http_major}.#{parser.http_minor}".to_f
      response.status = parser.status_code
      parser.headers.each { |field, value| response.headers.add(field, value) }
      return response
    end
  end # def read_http_message

  def write_http_body(body, io, chunked=false)
    if chunked
      write_http_body_chunked(body, io)
    else
      write_http_body_normal(body, io)
    end
  end # def write_http_body

  # Encode the given text as in 'chunked' encoding.
  def encode_chunked(text)
    return sprintf("%x%s%s%s", text.bytesize, CRLF, text, CRLF)
  end # def encode_chunked

  def write_http_body_chunked(body, io)
    if body.is_a?(String)
      write_all( io, encode_chunked(body))
    elsif body.respond_to?(:sysread)
      begin
        while cont = body.sysread(16384)
          write_all( io, encode_chunked(cont))
        end
      rescue EOFError
      end
    elsif body.respond_to?(:read)
      while cont = body.read(16384)
        write_all( io, encode_chunked(cont) )
      end
    elsif body.respond_to?(:each)
      body.each { |s| write_all( io, encode_chunked(s)) }
    end

    # The terminating chunk is an empty one.
    write_all(io, encode_chunked(""))
  end # def write_http_body_chunked

  def write_http_body_normal(body, io)
    if body.is_a?(String)
      write_all(io, body)
    elsif body.respond_to?(:read)
      while cont = body.read(16384)
        write_all(io, cont)
      end
    elsif body.respond_to?(:each)
      body.each { |s| write_all( io, s) }
    end
  end # def write_http_body_normal

  def write_all(io, string)
    while string.bytesize > 0
      w = io.write(string)
      string = string.byteslice(w..-1)
    end
  end # def write_all

  # Read the body of this message. The block is called with chunks of the
  # response as they are read in.
  #
  # This method is generally only called by http clients, not servers.
  def read_http_body(&block)
    if @body.respond_to?(:read)
      if headers.include?("Content-Length") and headers["Content-Length"].to_i > 0
        @logger.debug("Reading body with Content-Length")
        read_http_body_length(headers["Content-Length"].to_i, &block)
      elsif headers["Transfer-Encoding"] == "chunked"
        @logger.debug("Reading body with chunked encoding")
        read_http_body_chunked(&block)
      end

      # If this is a poolable resource, release it (like a FTW::Connection)
      @body.release if @body.respond_to?(:release)
    elsif !@body.nil?
      block.call(@body)
    end
  end # def read_http_body

  # Read the body of this message. The block is called with chunks of the
  # response as they are read in.
  #
  # This method is generally only called by http clients, not servers.
  #
  # If no block is given, the entire response body is returned as a string.
  def read_body(&block)
    if !block_given?
      content = ""
      read_http_body { |chunk| content << chunk }
      return content
    else
      read_http_body(&block)
    end
  end # def read_body

  # A shorthand for discarding the body of a request or response.
  #
  # This is the same as:
  #
  #     foo.read_body { |c| }
  def discard_body
    read_body { |c| }
  end # def discard_body

  # Read the length bytes from the body. Yield each chunk read to the block
  # given. This method is generally only called by http clients, not servers.
  def read_http_body_length(length, &block)
    remaining = length
    while remaining > 0
      data = @body.read(remaining)
      @logger.debug("Read bytes", :length => data.bytesize)
      if data.bytesize > remaining
        # Read too much data, only wanted part of this. Push the rest back.
        yield data[0..remaining]
        remaining = 0
        @body.pushback(data[remaining .. -1]) if remaining < 0
      else
        yield data
        remaining -= data.bytesize
      end
    end
  end # def read_http_body_length

  # This is kind of messed, need to fix it.
  def read_http_body_chunked(&block)
    parser = HTTP::Parser.new

    # Fake fill-in the response we've already read into the parser.
    parser << to_s
    parser << CRLF
    parser.on_body = block
    done = false
    parser.on_message_complete = proc { done = true }

    while !done # will break on special conditions below
      # TODO(sissel): In JRuby, this read will sometimes hang for ever
      # because there's some wonkiness in IO.select on SSLSockets in JRuby.
      # Maybe we should fix it... 
      data = @body.read
      offset = parser << data
      if offset != data.length
        raise "Parser did not consume all data read?"
      end
    end
  end # def read_http_body_chunked
end # module FTW::Protocol