File: response.rb

package info (click to toggle)
ruby-excon 0.112.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,232 kB
  • sloc: ruby: 7,855; makefile: 5
file content (247 lines) | stat: -rw-r--r-- 7,659 bytes parent folder | download
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# frozen_string_literal: true
module Excon
  class Response

    attr_accessor :data

    # backwards compatability reader/writers
    def body=(new_body)
      @data[:body] = new_body
    end
    def body
      @data[:body]
    end
    def headers=(new_headers)
      @data[:headers] = new_headers
    end
    def headers
      @data[:headers]
    end
    def host
      @data[:host]
    end
    def scheme
      @data[:scheme]
    end
    def local_address
      @data[:local_address]
    end
    def local_port
      @data[:local_port]
    end
    def http_method # can't be named "method"
      @data[:method]
    end
    def path
      @data[:path]
    end
    def query
      @data[:query]
    end
    def port
      @data[:port]
    end
    def reason_phrase=(new_reason_phrase)
      @data[:reason_phrase] = new_reason_phrase
    end
    def reason_phrase
      @data[:reason_phrase]
    end
    def remote_ip=(new_remote_ip)
      @data[:remote_ip] = new_remote_ip
    end
    def remote_ip
      @data[:remote_ip]
    end
    def status=(new_status)
      @data[:status] = new_status
    end
    def status
      @data[:status]
    end
    def status_line
      @data[:status_line]
    end
    def status_line=(new_status_line)
      @data[:status_line] = new_status_line
    end

    def self.parse(socket, datum)
      # this will discard any trailing lines from the previous response if any.
      line = nil
      loop do
        line = socket.readline
        break if line[9,3].to_i != 0
      end

      status = line[9, 3].to_i
      reason_phrase = line[13..-3] # -3 strips the trailing "\r\n"

      datum[:response] = {
        :body          => String.new,
        :cookies       => [],
        :host          => datum[:host],
        :scheme        => datum[:scheme],
        :method        => datum[:method],
        :headers       => Excon::Headers.new,
        :path          => datum[:path],
        :query         => datum[:query],
        :port          => datum[:port],
        :omit_default_port => datum[:omit_default_port],
        :status        => status,
        :status_line   => line,
        :reason_phrase => reason_phrase
      }

      unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false
      unless datum[:scheme] == UNIX || unix_proxy
        datum[:response].merge!(
          :remote_ip     => socket.remote_ip,
          :local_port    => socket.local_port,
          :local_address => socket.local_address
        )
      end

      parse_headers(socket, datum)

      unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status])

        if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 })
          encodings = Utils.split_header_value(datum[:response][:headers][key])
          if (encoding = encodings.last) && encoding.casecmp('chunked') == 0
            transfer_encoding_chunked = true
            if encodings.length == 1
              datum[:response][:headers].delete(key)
            else
              datum[:response][:headers][key] = encodings[0...-1].join(', ')
            end
          end
        end

        # use :response_block unless :expects would fail
        if (response_block = datum[:response_block])
          if datum[:middlewares].include?(Excon::Middleware::Expects) && datum[:expects] &&
                                !Array(datum[:expects]).include?(datum[:response][:status])
            response_block = nil
          end
        end

        if transfer_encoding_chunked
          if response_block
            while (chunk_size = socket.readline.chomp!.to_i(16)) > 0
              while chunk_size > 0
                chunk = socket.read(chunk_size) || raise(EOFError)
                chunk_size -= chunk.bytesize
                response_block.call(chunk, nil, nil)
              end
              new_line_size = 2 # 2 == "\r\n".length
              while new_line_size > 0
                chunk = socket.read(new_line_size) || raise(EOFError)
                new_line_size -= chunk.length
              end
            end
          else
            while (chunk_size = socket.readline.chomp!.to_i(16)) > 0
              while chunk_size > 0
                chunk = socket.read(chunk_size) || raise(EOFError)
                chunk_size -= chunk.bytesize
                datum[:response][:body] << chunk
              end
              new_line_size = 2 # 2 == "\r\n".length
              while new_line_size > 0
                chunk = socket.read(new_line_size) || raise(EOFError)
                new_line_size -= chunk.length
              end
            end
          end
          parse_headers(socket, datum) # merge trailers into headers
        else
          if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 })
            content_length = datum[:response][:headers][key].to_i
          end

          if (remaining = content_length)
            if response_block
              while remaining > 0
                chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
                response_block.call(chunk, [remaining - chunk.bytesize, 0].max, content_length)
                remaining -= chunk.bytesize
              end
            else
              while remaining > 0
                chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
                datum[:response][:body] << chunk
                remaining -= chunk.bytesize
              end
            end
          else
            if response_block
              while (chunk = socket.read(datum[:chunk_size]))
                response_block.call(chunk, nil, nil)
              end
            else
              while (chunk = socket.read(datum[:chunk_size]))
                datum[:response][:body] << chunk
              end
            end
          end
        end
      end
      datum
    end

    def self.parse_headers(socket, datum)
      last_key = nil
      until (data = socket.readline.chomp).empty?
        if !data.lstrip!.nil?
          raise Excon::Error::ResponseParse, 'malformed header' unless last_key
          # append to last_key's last value
          datum[:response][:headers][last_key] << ' ' << data.rstrip
        else
          key, value = data.split(':', 2)
          raise Excon::Error::ResponseParse, 'malformed header' unless value
          # add key/value or append value to existing values
          datum[:response][:headers][key] = ([datum[:response][:headers][key]] << value.strip).compact.join(', ')
          if key.casecmp('Set-Cookie') == 0
            datum[:response][:cookies] << value.strip
          end
          last_key = key
        end
      end
    end

    def initialize(params={})
      @data = {
        :body     => ''
      }.merge(params)
      @data[:headers] = Excon::Headers.new.merge!(params[:headers] || {})

      @body          = @data[:body]
      @headers       = @data[:headers]
      @status        = @data[:status]
      @remote_ip     = @data[:remote_ip]
      @local_port    = @data[:local_port]
      @local_address = @data[:local_address]
    end

    def [](key)
      @data[key]
    end

    def params
      Excon.display_warning('Excon::Response#params is deprecated use Excon::Response#data instead.')
      data
    end

    def pp
      Excon::PrettyPrinter.pp($stdout, @data)
    end

    # Retrieve a specific header value. Header names are treated case-insensitively.
    # @param [String] name Header name
    def get_header(name)
      headers[name]
    end

  end # class Response
end # module Excon